@peppermint-mcp/wizard 0.1.3 → 0.2.2
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/dist/cli.js +160 -9
- package/package.json +3 -2
- package/skills-bundle/peppermint/SKILL.md +205 -0
- package/skills-bundle/peppermint/identity-core.md +55 -0
- package/skills-bundle/peppermint/prompts/diff-categorize.md +41 -0
- package/skills-bundle/peppermint/prompts/parse-claims.md +59 -0
- package/skills-bundle/peppermint/prompts/reinforcement.md +50 -0
package/dist/cli.js
CHANGED
|
@@ -128,20 +128,28 @@ import { promisify as promisify2 } from "util";
|
|
|
128
128
|
var exec2 = promisify2(execFile2);
|
|
129
129
|
async function detectCodex() {
|
|
130
130
|
let version;
|
|
131
|
+
const warnings = [];
|
|
131
132
|
try {
|
|
132
133
|
const { stdout } = await exec2("codex", ["--version"], { timeout: 5e3 });
|
|
133
134
|
version = stdout.trim().split("\n")[0];
|
|
134
135
|
} catch {
|
|
135
136
|
return null;
|
|
136
137
|
}
|
|
138
|
+
let alreadyInstalled = false;
|
|
139
|
+
try {
|
|
140
|
+
const { stdout } = await exec2("codex", ["mcp", "list"], { timeout: 1e4 });
|
|
141
|
+
alreadyInstalled = stdout.includes("peppermint-memory") || stdout.includes("peppermint");
|
|
142
|
+
} catch {
|
|
143
|
+
warnings.push("Could not check existing MCP config");
|
|
144
|
+
}
|
|
137
145
|
return {
|
|
138
146
|
id: "codex",
|
|
139
147
|
name: "Codex CLI",
|
|
140
148
|
version,
|
|
141
149
|
installMethod: "cli",
|
|
142
|
-
alreadyInstalled
|
|
150
|
+
alreadyInstalled,
|
|
143
151
|
needsRestart: false,
|
|
144
|
-
warnings
|
|
152
|
+
warnings
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
155
|
|
|
@@ -259,7 +267,7 @@ async function createApiKey(serverBase2, accessToken) {
|
|
|
259
267
|
return data.raw_key;
|
|
260
268
|
}
|
|
261
269
|
function waitForCallback(port, expectedState) {
|
|
262
|
-
return new Promise((
|
|
270
|
+
return new Promise((resolve2, reject) => {
|
|
263
271
|
const timeout = setTimeout(() => {
|
|
264
272
|
server.close();
|
|
265
273
|
reject(new Error("Authentication timed out (5 minutes). Please try again."));
|
|
@@ -301,15 +309,15 @@ function waitForCallback(port, expectedState) {
|
|
|
301
309
|
);
|
|
302
310
|
clearTimeout(timeout);
|
|
303
311
|
server.close();
|
|
304
|
-
|
|
312
|
+
resolve2({ code });
|
|
305
313
|
});
|
|
306
314
|
server.listen(port, "127.0.0.1");
|
|
307
315
|
});
|
|
308
316
|
}
|
|
309
317
|
async function authenticateWithBrowser(serverBase2) {
|
|
310
318
|
const tempServer = createServer();
|
|
311
|
-
await new Promise((
|
|
312
|
-
tempServer.listen(0, "127.0.0.1", () =>
|
|
319
|
+
await new Promise((resolve2) => {
|
|
320
|
+
tempServer.listen(0, "127.0.0.1", () => resolve2());
|
|
313
321
|
});
|
|
314
322
|
const port = tempServer.address().port;
|
|
315
323
|
tempServer.close();
|
|
@@ -722,6 +730,129 @@ function checkHostConfig(hostId, configPath) {
|
|
|
722
730
|
}
|
|
723
731
|
}
|
|
724
732
|
|
|
733
|
+
// src/skills/index.ts
|
|
734
|
+
import {
|
|
735
|
+
copyFileSync as copyFileSync2,
|
|
736
|
+
existsSync as existsSync7,
|
|
737
|
+
mkdirSync as mkdirSync4,
|
|
738
|
+
readdirSync,
|
|
739
|
+
rmSync,
|
|
740
|
+
statSync
|
|
741
|
+
} from "fs";
|
|
742
|
+
import { homedir as homedir7 } from "os";
|
|
743
|
+
import { dirname as dirname3, join as join7, resolve } from "path";
|
|
744
|
+
import { fileURLToPath } from "url";
|
|
745
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
746
|
+
var LEGACY_SKILLS = [
|
|
747
|
+
"peppermint-recall",
|
|
748
|
+
"peppermint-capture",
|
|
749
|
+
"peppermint-ask-twin"
|
|
750
|
+
];
|
|
751
|
+
function getSkillsBundlePath() {
|
|
752
|
+
return resolve(__dirname, "..", "skills-bundle", "peppermint");
|
|
753
|
+
}
|
|
754
|
+
function getTargetPath() {
|
|
755
|
+
return join7(homedir7(), ".claude", "skills", "peppermint");
|
|
756
|
+
}
|
|
757
|
+
function copyDirRecursive(src, dest) {
|
|
758
|
+
mkdirSync4(dest, { recursive: true });
|
|
759
|
+
for (const entry of readdirSync(src)) {
|
|
760
|
+
const srcPath = join7(src, entry);
|
|
761
|
+
const destPath = join7(dest, entry);
|
|
762
|
+
if (statSync(srcPath).isDirectory()) {
|
|
763
|
+
copyDirRecursive(srcPath, destPath);
|
|
764
|
+
} else {
|
|
765
|
+
copyFileSync2(srcPath, destPath);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function removeLegacySkills(dryRun) {
|
|
770
|
+
const removed = [];
|
|
771
|
+
const skillsDir = join7(homedir7(), ".claude", "skills");
|
|
772
|
+
for (const name of LEGACY_SKILLS) {
|
|
773
|
+
const path = join7(skillsDir, name);
|
|
774
|
+
if (existsSync7(path)) {
|
|
775
|
+
if (!dryRun) {
|
|
776
|
+
rmSync(path, { recursive: true, force: true });
|
|
777
|
+
}
|
|
778
|
+
removed.push(name);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return removed;
|
|
782
|
+
}
|
|
783
|
+
function installSkills(dryRun) {
|
|
784
|
+
const bundlePath = getSkillsBundlePath();
|
|
785
|
+
const targetPath = getTargetPath();
|
|
786
|
+
const alreadyExists = existsSync7(join7(targetPath, "SKILL.md"));
|
|
787
|
+
if (!existsSync7(bundlePath)) {
|
|
788
|
+
return { installed: false, updated: false, targetPath, error: "Skills bundle not found in package" };
|
|
789
|
+
}
|
|
790
|
+
if (dryRun) {
|
|
791
|
+
return { installed: true, updated: alreadyExists, targetPath };
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
if (existsSync7(targetPath)) {
|
|
795
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
796
|
+
}
|
|
797
|
+
copyDirRecursive(bundlePath, targetPath);
|
|
798
|
+
return { installed: true, updated: alreadyExists, targetPath };
|
|
799
|
+
} catch (err) {
|
|
800
|
+
const message = err instanceof Error ? err.message : "Failed to install skills";
|
|
801
|
+
return { installed: false, updated: false, targetPath, error: message };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/skills/permissions.ts
|
|
806
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
807
|
+
import { homedir as homedir8 } from "os";
|
|
808
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
809
|
+
import * as jsonc5 from "jsonc-parser";
|
|
810
|
+
var REQUIRED_PERMISSIONS = [
|
|
811
|
+
"mcp__peppermint-memory__search",
|
|
812
|
+
"mcp__peppermint-memory__get",
|
|
813
|
+
"mcp__peppermint-memory__discover_tools",
|
|
814
|
+
"mcp__peppermint-memory__ask_twin",
|
|
815
|
+
"mcp__peppermint-memory__query_integration",
|
|
816
|
+
"mcp__peppermint-memory__create_memory",
|
|
817
|
+
"mcp__peppermint-memory__create_fact",
|
|
818
|
+
"mcp__peppermint-memory__update_memory"
|
|
819
|
+
];
|
|
820
|
+
function getSettingsPath() {
|
|
821
|
+
return join8(homedir8(), ".claude", "settings.json");
|
|
822
|
+
}
|
|
823
|
+
function installPermissions(dryRun) {
|
|
824
|
+
const settingsPath = getSettingsPath();
|
|
825
|
+
try {
|
|
826
|
+
let content = "";
|
|
827
|
+
if (existsSync8(settingsPath)) {
|
|
828
|
+
content = readFileSync7(settingsPath, "utf-8");
|
|
829
|
+
}
|
|
830
|
+
const parsed = content ? jsonc5.parse(content) : {};
|
|
831
|
+
const existingAllow = parsed?.permissions?.allow || [];
|
|
832
|
+
const toAdd = REQUIRED_PERMISSIONS.filter((p2) => !existingAllow.includes(p2));
|
|
833
|
+
if (toAdd.length === 0) {
|
|
834
|
+
return { added: [] };
|
|
835
|
+
}
|
|
836
|
+
if (dryRun) {
|
|
837
|
+
return { added: toAdd };
|
|
838
|
+
}
|
|
839
|
+
const newAllow = [...existingAllow, ...toAdd];
|
|
840
|
+
const edits = jsonc5.modify(content, ["permissions", "allow"], newAllow, {
|
|
841
|
+
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
842
|
+
});
|
|
843
|
+
const updated = jsonc5.applyEdits(content, edits);
|
|
844
|
+
const dir = dirname4(settingsPath);
|
|
845
|
+
if (!existsSync8(dir)) {
|
|
846
|
+
mkdirSync5(dir, { recursive: true });
|
|
847
|
+
}
|
|
848
|
+
writeFileSync4(settingsPath, updated, "utf-8");
|
|
849
|
+
return { added: toAdd };
|
|
850
|
+
} catch (err) {
|
|
851
|
+
const message = err instanceof Error ? err.message : "Failed to update permissions";
|
|
852
|
+
return { added: [], error: message };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
725
856
|
// src/cli.ts
|
|
726
857
|
var DEFAULT_SERVER = "https://api.peppermint.com/mcp/";
|
|
727
858
|
function serverBase(serverUrl) {
|
|
@@ -832,6 +963,28 @@ Check your internet connection and try again.`
|
|
|
832
963
|
p.log.info(` ${icon} ${host.name} ${pc.dim(result.message)}`);
|
|
833
964
|
results.push({ host, result });
|
|
834
965
|
}
|
|
966
|
+
const hasClaudeHost = hosts.some(
|
|
967
|
+
(h) => h.id === "claude-code" || h.id === "claude-desktop"
|
|
968
|
+
);
|
|
969
|
+
if (hasClaudeHost) {
|
|
970
|
+
const removed = removeLegacySkills(options.dryRun);
|
|
971
|
+
if (removed.length > 0) {
|
|
972
|
+
p.log.info(` ${pc.green("\u2713")} Removed legacy skills: ${pc.dim(removed.join(", "))}`);
|
|
973
|
+
}
|
|
974
|
+
const skillResult = installSkills(options.dryRun);
|
|
975
|
+
if (skillResult.installed) {
|
|
976
|
+
const verb = skillResult.updated ? "Updated" : "Installed";
|
|
977
|
+
p.log.info(` ${pc.green("\u2713")} ${verb} Peppermint skill ${pc.dim(skillResult.targetPath)}`);
|
|
978
|
+
} else if (skillResult.error) {
|
|
979
|
+
p.log.info(` ${pc.red("\u2717")} Skill install failed: ${pc.dim(skillResult.error)}`);
|
|
980
|
+
}
|
|
981
|
+
const permsResult = installPermissions(options.dryRun);
|
|
982
|
+
if (permsResult.error) {
|
|
983
|
+
p.log.info(` ${pc.red("\u2717")} Permissions: ${pc.dim(permsResult.error)}`);
|
|
984
|
+
} else if (permsResult.added.length > 0) {
|
|
985
|
+
p.log.info(` ${pc.green("\u2713")} Added ${permsResult.added.length} tool permissions to Claude Code settings`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
835
988
|
if (options.verify && !options.dryRun) {
|
|
836
989
|
p.log.step("Verifying...");
|
|
837
990
|
for (const { host } of results) {
|
|
@@ -843,9 +996,7 @@ Check your internet connection and try again.`
|
|
|
843
996
|
const needRestart = results.filter((r) => r.result.needsRestart);
|
|
844
997
|
const failed = results.filter((r) => !r.result.success);
|
|
845
998
|
p.outro(
|
|
846
|
-
failed.length > 0 ? pc.red(`${failed.length} host(s) failed. Check the output above.`) : needRestart.length > 0 ? pc.green("Done!") + pc.dim(
|
|
847
|
-
` Restart ${needRestart.map((r) => r.host.name).join(", ")} to finish.`
|
|
848
|
-
) : pc.green("Done! Peppermint MCP is ready.")
|
|
999
|
+
failed.length > 0 ? pc.red(`${failed.length} host(s) failed. Check the output above.`) : hasClaudeHost ? pc.green("Done!") + (needRestart.length > 0 ? pc.dim(` Restart ${needRestart.map((r) => r.host.name).join(", ")} to finish.`) : "") + "\n\n " + pc.cyan("Peppermint skill installed. Start a new Claude Code session and type") + "\n " + pc.cyan("@pep or /peppermint to begin onboarding.") : needRestart.length > 0 ? pc.green("Done!") + pc.dim(` Restart ${needRestart.map((r) => r.host.name).join(", ")} to finish.`) : pc.green("Done! Peppermint MCP is ready.")
|
|
849
1000
|
);
|
|
850
1001
|
if (failed.length > 0) process.exit(2);
|
|
851
1002
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peppermint-mcp/wizard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "One-command installer for Peppermint MCP across AI coding hosts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"peppermint-mcp-wizard": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist/"
|
|
10
|
+
"dist/",
|
|
11
|
+
"skills-bundle/"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsup",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peppermint
|
|
3
|
+
description: Peppermint memory companion — onboarding, @pep queries, passive capture.
|
|
4
|
+
when_to_trigger: |
|
|
5
|
+
- User types `@pep <...>` or `/peppermint <...>`
|
|
6
|
+
- User states a durable preference, rule, or identity claim
|
|
7
|
+
- User asks about prior work, past decisions, or historical context
|
|
8
|
+
- User references a teammate's work or asks what someone else is doing
|
|
9
|
+
allowed-tools:
|
|
10
|
+
- mcp__peppermint-memory__search
|
|
11
|
+
- mcp__peppermint-memory__get
|
|
12
|
+
- mcp__peppermint-memory__discover_tools
|
|
13
|
+
- mcp__peppermint-memory__create_memory
|
|
14
|
+
- mcp__peppermint-memory__create_fact
|
|
15
|
+
- mcp__peppermint-memory__update_memory
|
|
16
|
+
- mcp__peppermint-memory__ask_twin
|
|
17
|
+
- mcp__peppermint-memory__query_integration
|
|
18
|
+
- Read
|
|
19
|
+
- Write
|
|
20
|
+
- Edit
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Peppermint companion skill
|
|
24
|
+
|
|
25
|
+
You are an assistant with access to Peppermint — a persistent memory system that captures what the user sees, hears, reads, and works on across their devices. Peppermint stores memories, facts, daily summaries, and integration data (Linear, Asana, Slack, Google Workspace, GitHub).
|
|
26
|
+
|
|
27
|
+
This skill handles three modes: **onboarding** (first run), **@pep queries** (daily use), and **passive capture** (background).
|
|
28
|
+
|
|
29
|
+
## 0. Host detection
|
|
30
|
+
|
|
31
|
+
Detect the host environment:
|
|
32
|
+
- If the user has `~/.claude/CLAUDE.md` or you're running inside Claude Code → **claude-code**
|
|
33
|
+
- If you detect Codex CLI markers → **codex**
|
|
34
|
+
- If you detect Gemini markers → **gemini**
|
|
35
|
+
- Otherwise → **unknown**
|
|
36
|
+
|
|
37
|
+
For v1, only **claude-code** gets the full onboarding flow. All hosts get `@pep` queries and passive capture.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 1. Onboarding (claude-code only)
|
|
42
|
+
|
|
43
|
+
### 1.1 Detection — should onboarding run?
|
|
44
|
+
|
|
45
|
+
Check two signals:
|
|
46
|
+
1. **Server signal:** `search(query="onboarded claude-code", scope="facts", limit=1)` — look for a `skill_state` fact with subject `claude-code`
|
|
47
|
+
2. **File signal:** Check if `~/.claude/CLAUDE.md` contains `<!-- peppermint:onboarded`
|
|
48
|
+
|
|
49
|
+
| Server fact | File exists | Action |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Missing | Missing | Run full onboarding |
|
|
52
|
+
| Present | Present | Skip — print "Peppermint is connected. Use @pep to query your memory." |
|
|
53
|
+
| One missing | One present | Warn about inconsistent state, run onboarding |
|
|
54
|
+
|
|
55
|
+
### 1.2 Onboarding flow
|
|
56
|
+
|
|
57
|
+
1. **Greet:** "Setting up Peppermint memory sync for Claude Code."
|
|
58
|
+
|
|
59
|
+
2. **Parse host memory files:**
|
|
60
|
+
- Read `~/.claude/CLAUDE.md` (global)
|
|
61
|
+
- Glob `~/.claude/projects/*/memory/*.md` (project memories)
|
|
62
|
+
- Extract structured claims using the parse-claims sub-prompt (see `prompts/parse-claims.md`)
|
|
63
|
+
- Each claim becomes: `{content, inferred_family, confidence}`
|
|
64
|
+
|
|
65
|
+
3. **Diff against Peppermint graph:**
|
|
66
|
+
- For each batch of 10 claims, call `search(query=<claim>, scope="facts", limit=3)` in parallel
|
|
67
|
+
- Categorize each claim using the diff-categorize sub-prompt (see `prompts/diff-categorize.md`)
|
|
68
|
+
- Categories: ADD (new to Peppermint), UPDATE (refines existing), ADD-to-host (exists in Peppermint but not host), CONFLICT (contradicts), SKIP (trivial/duplicate)
|
|
69
|
+
|
|
70
|
+
4. **Present summary with top-3 examples per category:**
|
|
71
|
+
```
|
|
72
|
+
Found 23 claims in your Claude Code memory.
|
|
73
|
+
|
|
74
|
+
New to Peppermint (8):
|
|
75
|
+
- "Prefers mental-model-first explanations"
|
|
76
|
+
- "Uses uv for Python package management"
|
|
77
|
+
- "Working on Peppermint MCP server architecture"
|
|
78
|
+
... and 5 more
|
|
79
|
+
|
|
80
|
+
Already in Peppermint (12): synced
|
|
81
|
+
Conflicts (1): "Role: CTO" vs Peppermint fact "Role: Co-founder & CTO"
|
|
82
|
+
To add to Claude Code (2): ...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
5. **Confirm:** Ask "Want me to sync these? I'll save the new claims to Peppermint and update your identity core." Accept freeform confirmation.
|
|
86
|
+
|
|
87
|
+
6. **Commit:**
|
|
88
|
+
- Save ADD claims as facts: `create_fact(content=..., fact_family=..., fact_subject=...)`
|
|
89
|
+
- Save UPDATE claims as facts (upsert behavior)
|
|
90
|
+
- Resolve CONFLICTs per user instruction
|
|
91
|
+
- Append the identity core to `~/.claude/CLAUDE.md` inside a marker fence (see identity-core template). If a previous `<!-- peppermint:onboarded` block exists, replace it in-place. This ensures the identity is always in Claude Code's context window.
|
|
92
|
+
- Save onboarding marker: `create_fact(content="onboarded v=1 date=<today>", fact_family="skill_state", fact_subject="claude-code")`
|
|
93
|
+
|
|
94
|
+
7. **Post-onboarding training block:**
|
|
95
|
+
```
|
|
96
|
+
## How to use Peppermint from here on
|
|
97
|
+
|
|
98
|
+
**@pep <question>** — Pull memory into a decision in flight. This is the daily driver.
|
|
99
|
+
Examples: "@pep what was decided about the auth middleware?", "@pep what's PEP-371 status?"
|
|
100
|
+
|
|
101
|
+
**@pep ask <teammate> <question>** — Ask a teammate's digital twin.
|
|
102
|
+
Their twin answers from their memories. They're notified when their twin is queried.
|
|
103
|
+
Example: "@pep ask Sabrina what's the status of the PII layer fix?"
|
|
104
|
+
|
|
105
|
+
**@pep refresh** — Re-sync your identity core when things drift.
|
|
106
|
+
|
|
107
|
+
Peppermint also runs passively — it captures durable preferences and decisions
|
|
108
|
+
you state during normal work, without you having to invoke it.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
8. **5-section reinforcement summary:** Generate using `prompts/reinforcement.md` and display as a sanity check. Ask "Does this look right?"
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 2. @pep handler
|
|
116
|
+
|
|
117
|
+
Users invoke this via `@pep <question>`, `/peppermint <question>`, or natural language that references prior work/decisions.
|
|
118
|
+
|
|
119
|
+
### 2.1 Self-query (default): route through ask_twin
|
|
120
|
+
|
|
121
|
+
For any `@pep <question>` that isn't a sub-command:
|
|
122
|
+
|
|
123
|
+
1. Call `ask_twin(person="me", question="<the question>", context="<last 3-5 conversation turns if available>")`
|
|
124
|
+
2. This routes through the **exact same pipeline** as the Slack twin:
|
|
125
|
+
- 7 parallel backend queries (memories, facts, summaries, integrations, routines)
|
|
126
|
+
- 8-turn agentic Gemini tool loop with 11 tools
|
|
127
|
+
- Privacy domain filtering
|
|
128
|
+
- All attribution/commitment/identity rules
|
|
129
|
+
3. Strip the `[<name>'s Twin]` header from the response (self-queries don't need it)
|
|
130
|
+
4. Present the response directly to the user
|
|
131
|
+
|
|
132
|
+
**Context passing:** When the user has been in a multi-turn conversation before invoking `@pep`, include the last 3-5 turns as the `context` parameter. This helps the twin understand references like "tell me more about that" or "what about the other option?"
|
|
133
|
+
|
|
134
|
+
### 2.2 Sub-commands
|
|
135
|
+
|
|
136
|
+
**`@pep ask <person> <question>`**
|
|
137
|
+
1. Call `ask_twin(person="<person>", question="<question>", context="<conversation context>")`
|
|
138
|
+
2. Present with `[<person>'s Twin]` header
|
|
139
|
+
3. **Pass through near-verbatim** — the twin's response already follows attribution/commitment rules. Do NOT:
|
|
140
|
+
- Add commitments ("they'll get back to you")
|
|
141
|
+
- Re-attribute ("the team confirmed" when the twin said "Sabrina confirmed")
|
|
142
|
+
- Re-synthesize or editorialize
|
|
143
|
+
4. On error (twin declined, user not found, no context), show the specific error message from the tool
|
|
144
|
+
|
|
145
|
+
**`@pep refresh`**
|
|
146
|
+
1. Re-run the onboarding flow (§1.2) without the detection step
|
|
147
|
+
2. Replace the `<!-- peppermint:onboarded ... -->` block in `~/.claude/CLAUDE.md` with the new identity core
|
|
148
|
+
3. Update the `skill_state` fact with new date
|
|
149
|
+
|
|
150
|
+
**`@pep status`**
|
|
151
|
+
1. Call `get(kind="stats")` for memory stats
|
|
152
|
+
2. Call `search(query="connected", scope="facts", limit=5)` for integration status
|
|
153
|
+
3. Report: memory count, date range, connected integrations, last sync time
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 3. Passive capture
|
|
158
|
+
|
|
159
|
+
During normal conversation (not `@pep` invocations), watch for signals that the user stated something durable. Capture it silently.
|
|
160
|
+
|
|
161
|
+
### 3.1 Four-signal detection
|
|
162
|
+
|
|
163
|
+
A statement is worth capturing when ALL of these are true:
|
|
164
|
+
1. **Durable:** Would still be true next week (not "I'm debugging this right now")
|
|
165
|
+
2. **First-person:** The user is stating their own preference/decision/rule (not quoting someone)
|
|
166
|
+
3. **New or contradictory:** Not already captured in Peppermint (quick `search` check)
|
|
167
|
+
4. **Non-trivial:** Would change how an agent assists them ("I prefer TypeScript" yes; "I like coffee" no)
|
|
168
|
+
|
|
169
|
+
### 3.2 Don't capture list
|
|
170
|
+
|
|
171
|
+
Never capture:
|
|
172
|
+
- One-off debugging statements ("this variable is null")
|
|
173
|
+
- Emotional reactions ("this is frustrating")
|
|
174
|
+
- Questions the user is asking (they're seeking info, not stating facts)
|
|
175
|
+
- Code snippets or terminal output
|
|
176
|
+
- Anything about family, hobbies, entertainment, health, or personal life
|
|
177
|
+
|
|
178
|
+
### 3.3 Capture action
|
|
179
|
+
|
|
180
|
+
When a capturable signal is detected:
|
|
181
|
+
1. Activate the capture pack: `discover_tools(activate=["capture"])`
|
|
182
|
+
2. Determine if it's a new fact or memory:
|
|
183
|
+
- **Facts** (durable identity/preference/role): `create_fact(content=..., fact_family=..., fact_subject=...)`
|
|
184
|
+
- **Memories** (decisions, commitments, project context): `create_memory(content=..., tags=[...], importance=0.7)`
|
|
185
|
+
3. If contradicting an existing fact: note inline — "Updated your preference from X to Y in Peppermint."
|
|
186
|
+
4. If new: capture silently (no notification to user)
|
|
187
|
+
|
|
188
|
+
### 3.4 Budget
|
|
189
|
+
|
|
190
|
+
- Max 1 capture per conversation turn
|
|
191
|
+
- Max 15 captures per session
|
|
192
|
+
- If budget exhausted, stop watching (don't queue)
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 4. Error handling
|
|
197
|
+
|
|
198
|
+
| Error | Action |
|
|
199
|
+
|---|---|
|
|
200
|
+
| MCP server unreachable | Print "Peppermint is offline — memory features unavailable this session." Disable capture for the session. |
|
|
201
|
+
| `ask_twin` returns decline | Print "[person]'s twin doesn't have enough context to answer that right now." |
|
|
202
|
+
| `ask_twin` user not found | Print "No one matching '[person]' found in your organization." |
|
|
203
|
+
| `ask_twin` no org | Print "You need to be part of an organization to query teammates' twins." |
|
|
204
|
+
| `search` returns empty | Answer from conversation context alone, note "No relevant memories found in Peppermint." |
|
|
205
|
+
| Onboarding — < 10 facts | Run onboarding but note: "Your Peppermint memory is still building up. The more you use your devices with Peppermint running, the richer your memory gets." |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Identity core template
|
|
2
|
+
|
|
3
|
+
This template is used by the Peppermint skill to generate the identity core block
|
|
4
|
+
appended to `~/.claude/CLAUDE.md`. The skill fills in each section from the user's
|
|
5
|
+
Peppermint fact graph, wraps it in marker fences, and appends it to CLAUDE.md
|
|
6
|
+
(or replaces an existing block on refresh).
|
|
7
|
+
|
|
8
|
+
## Output format
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
<!-- peppermint:onboarded v=1 date=YYYY-MM-DD -->
|
|
12
|
+
## Peppermint identity core
|
|
13
|
+
|
|
14
|
+
### Who you are at work
|
|
15
|
+
- Role: {current_role fact}
|
|
16
|
+
- Company: {current_employer fact}
|
|
17
|
+
- Department: {department fact}
|
|
18
|
+
- Expertise: {expertise facts, comma-separated}
|
|
19
|
+
|
|
20
|
+
### How you work
|
|
21
|
+
- {preference facts — tools, languages, workflows}
|
|
22
|
+
- {routine facts — daily patterns, work hours}
|
|
23
|
+
|
|
24
|
+
### Who you work with
|
|
25
|
+
- {team_membership facts — direct collaborators, max 5}
|
|
26
|
+
- {reporting_structure facts}
|
|
27
|
+
|
|
28
|
+
### What you're working on
|
|
29
|
+
- {current_work facts, max 5 most recent}
|
|
30
|
+
- {project_assignments facts}
|
|
31
|
+
- {current_blocker facts if any}
|
|
32
|
+
|
|
33
|
+
### Rules and preferences
|
|
34
|
+
- {preference facts that are rules — "always do X", "never do Y"}
|
|
35
|
+
- {identity facts that constrain behavior}
|
|
36
|
+
<!-- peppermint:end -->
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Token budget
|
|
40
|
+
|
|
41
|
+
Target: 500-800 tokens. Hard cap: 800 tokens.
|
|
42
|
+
|
|
43
|
+
### Priority cuts (when over budget)
|
|
44
|
+
|
|
45
|
+
1. Cut oldest "What you're working on" items (keep max 5)
|
|
46
|
+
2. Cut collaborators beyond top 5
|
|
47
|
+
3. Cut lowest-frequency rules/preferences
|
|
48
|
+
4. Cut department/reporting_structure if space is tight
|
|
49
|
+
|
|
50
|
+
### Work-relevance test
|
|
51
|
+
|
|
52
|
+
For each fact, ask: "Would knowing this change the agent's advice on a coding task?"
|
|
53
|
+
|
|
54
|
+
**Include:** role, tools, languages, frameworks, team, active projects, coding preferences, communication style
|
|
55
|
+
**Exclude:** family, hobbies, entertainment, health, food preferences, personal life
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Diff and categorize claims against Peppermint graph
|
|
2
|
+
|
|
3
|
+
Given a set of claims extracted from host memory files and the Peppermint search results for each, categorize what action to take.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
For each claim, you receive:
|
|
8
|
+
- The claim: `{content, inferred_family, confidence}`
|
|
9
|
+
- Search results from Peppermint: 0-3 matching facts with content and similarity
|
|
10
|
+
|
|
11
|
+
## Categories
|
|
12
|
+
|
|
13
|
+
- **ADD** — Claim is new. No matching fact in Peppermint. Save it.
|
|
14
|
+
- **UPDATE** — Claim refines or updates an existing Peppermint fact (same topic, newer/more specific info). Save as update.
|
|
15
|
+
- **ADD-to-host** — Peppermint has this fact but the host memory file doesn't. Note for the user (don't auto-write to host files).
|
|
16
|
+
- **CONFLICT** — Claim contradicts a Peppermint fact (e.g., different role, different preference). Flag for user resolution.
|
|
17
|
+
- **SKIP** — Claim is a near-duplicate of an existing fact, or too low-confidence to act on.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
1. **Semantic matching, not string matching.** "Uses TypeScript" and "Prefers TypeScript for new projects" are the same fact (SKIP or UPDATE depending on specificity).
|
|
22
|
+
|
|
23
|
+
2. **Recency wins for current_work, current_blocker, project_assignments.** If the claim is more recent than the Peppermint fact, categorize as UPDATE.
|
|
24
|
+
|
|
25
|
+
3. **CONFLICTs require genuine contradiction.** "CTO" vs "Co-founder & CTO" is not a conflict — it's an UPDATE (more specific). "CTO" vs "VP Engineering" IS a conflict.
|
|
26
|
+
|
|
27
|
+
4. **Low-confidence claims:** If `confidence=low` and there's no matching Peppermint fact, categorize as SKIP (don't pollute the graph with weak signals).
|
|
28
|
+
|
|
29
|
+
5. **ADD-to-host is informational only.** The skill doesn't auto-write to CLAUDE.md or project memory files. It just tells the user "Peppermint knows X that your Claude Code memory doesn't."
|
|
30
|
+
|
|
31
|
+
## Output
|
|
32
|
+
|
|
33
|
+
For each claim, output:
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"claim": "...",
|
|
37
|
+
"category": "ADD | UPDATE | ADD-to-host | CONFLICT | SKIP",
|
|
38
|
+
"reason": "one-line explanation",
|
|
39
|
+
"matching_fact_id": "uuid or null"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Parse claims from host memory files
|
|
2
|
+
|
|
3
|
+
Given the content of a user's memory file (CLAUDE.md, project memory, etc.), extract structured claims about the user.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
The raw text content of one or more memory files.
|
|
8
|
+
|
|
9
|
+
## Output
|
|
10
|
+
|
|
11
|
+
A JSON array of claims:
|
|
12
|
+
```json
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
"content": "Prefers TypeScript over JavaScript for new projects",
|
|
16
|
+
"inferred_family": "preference",
|
|
17
|
+
"confidence": "high"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"content": "Currently working on Peppermint MCP server architecture",
|
|
21
|
+
"inferred_family": "current_work",
|
|
22
|
+
"confidence": "medium"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
1. **Extract only durable claims** — skip ephemeral task notes, debugging context, and conversation-specific instructions.
|
|
30
|
+
|
|
31
|
+
2. **Map to fact families:**
|
|
32
|
+
- `identity` — who the user is (name, background, education)
|
|
33
|
+
- `preference` — how they like things done (tools, patterns, styles, rules)
|
|
34
|
+
- `routine` — recurring patterns (daily standup time, review process)
|
|
35
|
+
- `current_work` — what they're actively building/fixing
|
|
36
|
+
- `current_employer` — company name
|
|
37
|
+
- `current_role` — job title
|
|
38
|
+
- `current_blocker` — things blocking progress
|
|
39
|
+
- `team_membership` — direct collaborators
|
|
40
|
+
- `reporting_structure` — who reports to whom
|
|
41
|
+
- `department` — organizational unit
|
|
42
|
+
- `expertise` — skills and domain knowledge
|
|
43
|
+
- `project_assignments` — projects they're on
|
|
44
|
+
|
|
45
|
+
3. **Confidence levels:**
|
|
46
|
+
- `high` — explicitly stated ("I prefer X", "Always use Y")
|
|
47
|
+
- `medium` — strongly implied by context (listed in preferences section)
|
|
48
|
+
- `low` — weakly implied (mentioned once in passing)
|
|
49
|
+
|
|
50
|
+
4. **Skip these:**
|
|
51
|
+
- Code snippets and command examples (these are documentation, not claims)
|
|
52
|
+
- Tool configuration details (file paths, API endpoints)
|
|
53
|
+
- Instructions to the AI that aren't about the user ("don't use emojis" is a preference; "respond in JSON" is a formatting instruction)
|
|
54
|
+
|
|
55
|
+
5. **Normalize:** Write claims as standalone sentences that make sense without the source file context.
|
|
56
|
+
|
|
57
|
+
6. **Deduplicate:** If the same fact appears multiple times across files, emit it once with the highest confidence.
|
|
58
|
+
|
|
59
|
+
7. **Work-relevance filter:** Would knowing this change the agent's advice on a work task? If no, skip it. Family, hobbies, entertainment, health, and personal life are always excluded.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generate 5-section reinforcement summary
|
|
2
|
+
|
|
3
|
+
After onboarding, generate a human-readable summary of what Peppermint knows about the user. This serves as a sanity check — the user reviews it and corrects anything wrong.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
The user's Peppermint facts, organized by family. Fetched via:
|
|
8
|
+
- `search(query="identity role employer", scope="facts", limit=20)`
|
|
9
|
+
- `search(query="preference routine workflow", scope="facts", limit=20)`
|
|
10
|
+
- `search(query="team collaborator manager", scope="facts", limit=20)`
|
|
11
|
+
- `search(query="current work project blocker", scope="facts", limit=20)`
|
|
12
|
+
|
|
13
|
+
## Output format
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Here's what Peppermint knows about you:
|
|
17
|
+
|
|
18
|
+
**Who you are at work**
|
|
19
|
+
[Role, company, department, expertise — 2-4 bullets]
|
|
20
|
+
|
|
21
|
+
**How you work**
|
|
22
|
+
[Tools, languages, workflows, patterns — 3-5 bullets]
|
|
23
|
+
|
|
24
|
+
**Who you work with**
|
|
25
|
+
[Key collaborators, team, reporting — 2-4 bullets]
|
|
26
|
+
|
|
27
|
+
**What you're working on**
|
|
28
|
+
[Active projects, current focus — 2-5 bullets]
|
|
29
|
+
|
|
30
|
+
**Rules and preferences**
|
|
31
|
+
[Coding style, communication preferences, hard rules — 2-5 bullets]
|
|
32
|
+
|
|
33
|
+
Does this look right? Tell me if anything is wrong or missing.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
1. **Natural language, not raw facts.** Don't say "fact_family: preference, content: uses uv". Say "You use uv for Python package management."
|
|
39
|
+
|
|
40
|
+
2. **Group related facts.** If there are 3 facts about Python tooling, combine them into one bullet.
|
|
41
|
+
|
|
42
|
+
3. **Prioritize by work relevance.** Lead each section with the most impactful facts.
|
|
43
|
+
|
|
44
|
+
4. **Cap each section at 5 bullets.** If more facts exist, pick the highest-value ones.
|
|
45
|
+
|
|
46
|
+
5. **Work-relevance filter.** Exclude family, hobbies, entertainment, health. If a fact slipped through, drop it silently.
|
|
47
|
+
|
|
48
|
+
6. **Acknowledge gaps.** If a section is empty (e.g., no team_membership facts), say "Not enough data yet — this section will fill in as Peppermint learns more."
|
|
49
|
+
|
|
50
|
+
7. **Tone:** Direct, professional, second-person ("You work at...", "Your team includes..."). Not formal, not casual.
|