@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.0
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/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
- package/LICENSE +1 -1
- package/README.md +88 -37
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +130 -33
- package/cli/buddy-shell.ts +1 -1
- package/{adapters/claude/install → cli}/disable.ts +19 -32
- package/cli/doctor.ts +231 -0
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +30 -26
- package/{adapters/claude/install → cli}/install.ts +65 -83
- package/{adapters/claude/install → cli}/pick.ts +111 -50
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
- package/cli/tui.tsx +106 -17
- package/{adapters/claude/install → cli}/uninstall.ts +40 -47
- package/cli/upgrade.ts +2 -2
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +17 -1
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -15
- package/server/art.ts +450 -1
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/{adapters/claude/server → server}/index.ts +556 -95
- package/server/mcp-launcher.sh +16 -6
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +70 -18
- package/server/shift-mood.ts +43 -0
- package/server/state.ts +540 -3
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
- package/statusline/buddy-status.sh +397 -0
- package/statusline/combined-status.sh +134 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -207
- package/adapters/claude/install/doctor.ts +0 -172
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/path.ts +0 -12
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -408
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -33
- package/adapters/claude/storage/settings.ts +0 -91
- package/adapters/claude/storage/state.ts +0 -365
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
- package/server/engine.ts +0 -1
- package/server/reactions.ts +0 -1
package/server/mcp-launcher.sh
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# coding-buddy MCP server launcher — preflight checks bun, then execs the server.
|
|
3
|
+
# Failing here with a clear stderr message is much easier to debug than a silent
|
|
4
|
+
# "MCP tools not available" from Claude Code.
|
|
5
|
+
|
|
2
6
|
set -eu
|
|
7
|
+
|
|
3
8
|
if ! command -v bun >/dev/null 2>&1; then
|
|
4
|
-
cat >&2 <<'
|
|
5
|
-
[
|
|
9
|
+
cat >&2 <<'EOF'
|
|
10
|
+
[coding-buddy] ERROR: 'bun' was not found on PATH.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
coding-buddy's MCP server runs on bun. Install it with:
|
|
8
13
|
|
|
14
|
+
Linux / macOS:
|
|
9
15
|
curl -fsSL https://bun.sh/install | bash
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
Windows (PowerShell):
|
|
18
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
19
|
+
|
|
20
|
+
Then open a new shell (so PATH picks up bun) and restart Claude Code.
|
|
21
|
+
EOF
|
|
13
22
|
exit 127
|
|
14
23
|
fi
|
|
24
|
+
|
|
15
25
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
-
exec bun "$SCRIPT_DIR
|
|
26
|
+
exec bun "$SCRIPT_DIR/index.ts"
|
package/server/memory.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-session memory — remembers past projects and earlier conversations.
|
|
3
|
+
*
|
|
4
|
+
* Memory is stored in ~/.claude-buddy/memory/:
|
|
5
|
+
* projects.json — project records
|
|
6
|
+
* bugs.json — encountered bugs
|
|
7
|
+
* preferences.json — user preferences
|
|
8
|
+
*
|
|
9
|
+
* All data is local-only. Nothing is sent to any remote service.
|
|
10
|
+
* Privacy: users can wipe with `rm -rf ~/.claude-buddy/memory`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
readFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
} from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { buddyStateDir } from "./path";
|
|
21
|
+
|
|
22
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface ProjectMemory {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
path: string;
|
|
28
|
+
language: string[]; // e.g. ["typescript", "python"]
|
|
29
|
+
framework?: string;
|
|
30
|
+
lastSeen: number; // timestamp
|
|
31
|
+
noteCount: number;
|
|
32
|
+
tags: string[];
|
|
33
|
+
fileCount?: number; // number of files in project
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BugMemory {
|
|
37
|
+
id: string;
|
|
38
|
+
projectId: string;
|
|
39
|
+
errorSignature: string; // hash of error message
|
|
40
|
+
summary: string;
|
|
41
|
+
resolved: boolean;
|
|
42
|
+
resolvedAt?: number;
|
|
43
|
+
occurrenceCount: number;
|
|
44
|
+
lastSeen: number;
|
|
45
|
+
firstSeen: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PreferenceMemory {
|
|
49
|
+
key: string;
|
|
50
|
+
value: string;
|
|
51
|
+
context: string;
|
|
52
|
+
confidence: number; // 0-1, derived from frequency
|
|
53
|
+
lastUpdated: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── File paths ───────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const MEMORY_DIR = join(buddyStateDir(), "memory");
|
|
59
|
+
const PROJECTS_FILE = join(MEMORY_DIR, "projects.json");
|
|
60
|
+
const BUGS_FILE = join(MEMORY_DIR, "bugs.json");
|
|
61
|
+
const PREFERENCES_FILE = join(MEMORY_DIR, "preferences.json");
|
|
62
|
+
|
|
63
|
+
function ensureDir(): void {
|
|
64
|
+
mkdirSync(MEMORY_DIR, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadJson<T>(path: string, fallback: T): T {
|
|
68
|
+
try {
|
|
69
|
+
if (existsSync(path)) {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// corrupted or empty — use fallback
|
|
74
|
+
}
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function saveJson<T>(path: string, data: T): void {
|
|
79
|
+
ensureDir();
|
|
80
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Project memory ──────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export function loadProjects(): Record<string, ProjectMemory> {
|
|
86
|
+
return loadJson(PROJECTS_FILE, {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function saveProjects(projects: Record<string, ProjectMemory>): void {
|
|
90
|
+
saveJson(PROJECTS_FILE, projects);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getProject(id: string): ProjectMemory | null {
|
|
94
|
+
const projects = loadProjects();
|
|
95
|
+
return projects[id] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function updateProject(
|
|
99
|
+
id: string,
|
|
100
|
+
updates: Partial<Omit<ProjectMemory, "id">>,
|
|
101
|
+
): ProjectMemory {
|
|
102
|
+
const projects = loadProjects();
|
|
103
|
+
const existing = projects[id] ?? {
|
|
104
|
+
id,
|
|
105
|
+
name: id,
|
|
106
|
+
path: "",
|
|
107
|
+
language: [],
|
|
108
|
+
lastSeen: Date.now(),
|
|
109
|
+
noteCount: 0,
|
|
110
|
+
tags: [],
|
|
111
|
+
};
|
|
112
|
+
projects[id] = {
|
|
113
|
+
...existing,
|
|
114
|
+
...updates,
|
|
115
|
+
id, // always preserve original id
|
|
116
|
+
lastSeen: Date.now(),
|
|
117
|
+
};
|
|
118
|
+
saveProjects(projects);
|
|
119
|
+
return projects[id];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Bug memory ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export function loadBugs(): Record<string, BugMemory> {
|
|
125
|
+
return loadJson(BUGS_FILE, {});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function saveBugs(bugs: Record<string, BugMemory>): void {
|
|
129
|
+
saveJson(BUGS_FILE, bugs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getBug(id: string): BugMemory | null {
|
|
133
|
+
const bugs = loadBugs();
|
|
134
|
+
return bugs[id] ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Find a bug by its error signature hash within a project */
|
|
138
|
+
export function findBugBySignature(
|
|
139
|
+
projectId: string,
|
|
140
|
+
errorSignature: string,
|
|
141
|
+
): BugMemory | null {
|
|
142
|
+
const bugs = loadBugs();
|
|
143
|
+
return Object.values(bugs).find(
|
|
144
|
+
(b) => b.projectId === projectId && b.errorSignature === errorSignature,
|
|
145
|
+
) ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function addBugMemory(
|
|
149
|
+
projectId: string,
|
|
150
|
+
errorSignature: string,
|
|
151
|
+
summary: string,
|
|
152
|
+
): BugMemory {
|
|
153
|
+
const bugs = loadBugs();
|
|
154
|
+
|
|
155
|
+
// Check if this bug already exists
|
|
156
|
+
const existing = findBugBySignature(projectId, errorSignature);
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.occurrenceCount += 1;
|
|
159
|
+
existing.lastSeen = Date.now();
|
|
160
|
+
existing.summary = summary; // update summary with latest
|
|
161
|
+
saveBugs(bugs);
|
|
162
|
+
return existing;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const id = `${projectId}-${errorSignature.slice(0, 8)}`;
|
|
166
|
+
const bug: BugMemory = {
|
|
167
|
+
id,
|
|
168
|
+
projectId,
|
|
169
|
+
errorSignature,
|
|
170
|
+
summary,
|
|
171
|
+
resolved: false,
|
|
172
|
+
occurrenceCount: 1,
|
|
173
|
+
lastSeen: Date.now(),
|
|
174
|
+
firstSeen: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
bugs[id] = bug;
|
|
177
|
+
saveBugs(bugs);
|
|
178
|
+
return bug;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveBug(id: string): BugMemory | null {
|
|
182
|
+
const bugs = loadBugs();
|
|
183
|
+
const bug = bugs[id];
|
|
184
|
+
if (!bug) return null;
|
|
185
|
+
bug.resolved = true;
|
|
186
|
+
bug.resolvedAt = Date.now();
|
|
187
|
+
saveBugs(bugs);
|
|
188
|
+
return bug;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Preference memory ─────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export function loadPreferences(): Record<string, PreferenceMemory> {
|
|
194
|
+
return loadJson(PREFERENCES_FILE, {});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function savePreferences(prefs: Record<string, PreferenceMemory>): void {
|
|
198
|
+
saveJson(PREFERENCES_FILE, prefs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function setPreference(
|
|
202
|
+
key: string,
|
|
203
|
+
value: string,
|
|
204
|
+
context: string,
|
|
205
|
+
confidenceBoost: number = 0.1,
|
|
206
|
+
): PreferenceMemory {
|
|
207
|
+
const prefs = loadPreferences();
|
|
208
|
+
const existing = prefs[key];
|
|
209
|
+
|
|
210
|
+
if (existing && existing.context === context) {
|
|
211
|
+
// Same context — increase confidence, update value
|
|
212
|
+
existing.confidence = Math.min(1, existing.confidence + confidenceBoost);
|
|
213
|
+
existing.value = value;
|
|
214
|
+
existing.lastUpdated = Date.now();
|
|
215
|
+
} else {
|
|
216
|
+
// New context or different context — create new or merge cautiously
|
|
217
|
+
const newConf = confidenceBoost;
|
|
218
|
+
if (existing && existing.confidence > newConf) {
|
|
219
|
+
// Existing is more confident — don't overwrite
|
|
220
|
+
existing.lastUpdated = Date.now();
|
|
221
|
+
} else {
|
|
222
|
+
prefs[key] = {
|
|
223
|
+
key,
|
|
224
|
+
value,
|
|
225
|
+
context,
|
|
226
|
+
confidence: newConf,
|
|
227
|
+
lastUpdated: Date.now(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
savePreferences(prefs);
|
|
233
|
+
return prefs[key];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Consolidation ───────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/** Simple hash function for error signatures */
|
|
239
|
+
function simpleHash(s: string): string {
|
|
240
|
+
let h = 0;
|
|
241
|
+
for (let i = 0; i < s.length; i++) {
|
|
242
|
+
const char = s.charCodeAt(i);
|
|
243
|
+
h = ((h << 5) - h) + char;
|
|
244
|
+
h = h & h; // convert to 32-bit integer
|
|
245
|
+
}
|
|
246
|
+
return Math.abs(h).toString(16);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Extract language signals from text (file extensions) */
|
|
250
|
+
function extractLanguages(text: string): string[] {
|
|
251
|
+
const extMap: Record<string, string> = {
|
|
252
|
+
ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript",
|
|
253
|
+
py: "python", rs: "rust", go: "go", java: "java", rb: "ruby",
|
|
254
|
+
php: "php", c: "c", cpp: "cpp", cs: "csharp", swift: "swift",
|
|
255
|
+
kt: "kotlin", scala: "scala", r: "r", lua: "lua", pl: "perl",
|
|
256
|
+
sh: "shell", bash: "shell", zsh: "shell", fish: "shell",
|
|
257
|
+
sql: "sql", html: "html", css: "css", scss: "scss", json: "json",
|
|
258
|
+
yaml: "yaml", yml: "yaml", toml: "toml", xml: "xml", md: "markdown",
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const found = new Set<string>();
|
|
262
|
+
const extRegex = /\.([a-z]+)/gi;
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = extRegex.exec(text)) !== null) {
|
|
265
|
+
const ext = match[1].toLowerCase();
|
|
266
|
+
const lang = extMap[ext];
|
|
267
|
+
if (lang) found.add(lang);
|
|
268
|
+
}
|
|
269
|
+
return [...found];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Extract package manager / framework signals */
|
|
273
|
+
function extractFramework(text: string): string | undefined {
|
|
274
|
+
const signals: Record<string, string> = {
|
|
275
|
+
"package.json": "Node.js", "package-lock.json": "Node.js",
|
|
276
|
+
"requirements.txt": "Python", "Pipfile": "Python", "pyproject.toml": "Python",
|
|
277
|
+
"Cargo.toml": "Rust", "Cargo.lock": "Rust",
|
|
278
|
+
"go.mod": "Go", "go.sum": "Go",
|
|
279
|
+
"pom.xml": "Java", "build.gradle": "Java",
|
|
280
|
+
"Gemfile": "Ruby", "Gemfile.lock": "Ruby",
|
|
281
|
+
"composer.json": "PHP",
|
|
282
|
+
"swiftpm": "Swift", "Package.swift": "Swift",
|
|
283
|
+
".NET": ".NET", "csproj": ".NET",
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (const [file, framework] of Object.entries(signals)) {
|
|
287
|
+
if (text.includes(file)) return framework;
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Extract project name from path or git remote */
|
|
293
|
+
function extractProjectName(text: string): string | undefined {
|
|
294
|
+
// Try to find a directory path
|
|
295
|
+
const pathRegex = /\/([^\/]+)\/(?:src|lib|app|packages|tests?)/gi;
|
|
296
|
+
let match;
|
|
297
|
+
while ((match = pathRegex.exec(text)) !== null) {
|
|
298
|
+
return match[1];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Try git remote
|
|
302
|
+
const gitRegex = /github\.com[:/]([\w\-\.]+)\/([^\/\s]+)/gi;
|
|
303
|
+
while ((match = gitRegex.exec(text)) !== null) {
|
|
304
|
+
return match[2];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Extract error messages from text */
|
|
311
|
+
function extractErrors(text: string): Array<{ signature: string; summary: string }> {
|
|
312
|
+
const errors: Array<{ signature: string; summary: string }> = [];
|
|
313
|
+
// Common error patterns
|
|
314
|
+
const errorPatterns = [
|
|
315
|
+
/([A-Z][a-z]+Error: .+)/,
|
|
316
|
+
/(SyntaxError: .+)/,
|
|
317
|
+
/(TypeError: .+)/,
|
|
318
|
+
/(ReferenceError: .+)/,
|
|
319
|
+
/(Error: .+)/,
|
|
320
|
+
/(panic: .+)/,
|
|
321
|
+
/(Exception: .+)/,
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
for (const pattern of errorPatterns) {
|
|
325
|
+
let match;
|
|
326
|
+
const regex = new RegExp(pattern, "gi");
|
|
327
|
+
while ((match = regex.exec(text)) !== null) {
|
|
328
|
+
const full = match[0];
|
|
329
|
+
const signature = simpleHash(full);
|
|
330
|
+
// Truncate summary for storage
|
|
331
|
+
const summary = full.slice(0, 200);
|
|
332
|
+
errors.push({ signature, summary });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return errors;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Consolidate memory from a conversation turn.
|
|
341
|
+
* Called on Stop hook with the assistant's response and user prompt.
|
|
342
|
+
*/
|
|
343
|
+
export function consolidateMemory(
|
|
344
|
+
assistantMessage: string,
|
|
345
|
+
userPrompt: string,
|
|
346
|
+
): void {
|
|
347
|
+
const combined = `${assistantMessage} ${userPrompt}`;
|
|
348
|
+
|
|
349
|
+
// Extract project name
|
|
350
|
+
const projectName = extractProjectName(combined);
|
|
351
|
+
if (!projectName) return;
|
|
352
|
+
|
|
353
|
+
// Extract and update project
|
|
354
|
+
const languages = extractLanguages(combined);
|
|
355
|
+
const framework = extractFramework(combined);
|
|
356
|
+
updateProject(projectName, {
|
|
357
|
+
name: projectName,
|
|
358
|
+
language: languages.length > 0 ? languages : undefined,
|
|
359
|
+
framework,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Extract and store bugs
|
|
363
|
+
const errors = extractErrors(combined);
|
|
364
|
+
for (const err of errors) {
|
|
365
|
+
addBugMemory(projectName, err.signature, err.summary);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Retrieval ───────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
export interface MemoryQuery {
|
|
372
|
+
project?: string;
|
|
373
|
+
type?: "projects" | "bugs" | "preferences" | "all";
|
|
374
|
+
resolved?: boolean; // for bugs
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function queryMemory(query: MemoryQuery): {
|
|
378
|
+
projects: ProjectMemory[];
|
|
379
|
+
bugs: BugMemory[];
|
|
380
|
+
preferences: PreferenceMemory[];
|
|
381
|
+
} {
|
|
382
|
+
const projects = query.type === "bugs" || query.type === "preferences"
|
|
383
|
+
? []
|
|
384
|
+
: Object.values(loadProjects()).filter(
|
|
385
|
+
(p) => !query.project || p.id === query.project || p.name.includes(query.project),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
let bugs = query.type === "projects" || query.type === "preferences"
|
|
389
|
+
? []
|
|
390
|
+
: Object.values(loadBugs());
|
|
391
|
+
|
|
392
|
+
if (query.project) {
|
|
393
|
+
bugs = bugs.filter((b) => b.projectId === query.project);
|
|
394
|
+
}
|
|
395
|
+
if (query.resolved !== undefined) {
|
|
396
|
+
bugs = bugs.filter((b) => b.resolved === query.resolved);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const preferences = query.type === "projects" || query.type === "bugs"
|
|
400
|
+
? []
|
|
401
|
+
: Object.values(loadPreferences());
|
|
402
|
+
|
|
403
|
+
return { projects, bugs, preferences };
|
|
404
|
+
}
|
package/server/mood.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mood system — buddy情绪 based on coding events and time of day.
|
|
3
|
+
*
|
|
4
|
+
* Mood is session-scoped (stored in mood.json, resets on session restart).
|
|
5
|
+
* Shifts based on: test results, error count, session duration, time of day.
|
|
6
|
+
* Affects: reaction flavor, stat modifiers, art expression.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { buddyStateDir } from "./path";
|
|
12
|
+
import type { StatName } from "../core/engine.ts"
|
|
13
|
+
|
|
14
|
+
// ─── Mood types ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export type Mood =
|
|
17
|
+
| "happy" // tests passing, good code
|
|
18
|
+
| "focused" // active coding session, good progress
|
|
19
|
+
| "excited" // big diff, achievement unlocked, major event
|
|
20
|
+
| "tired" // long session, many errors
|
|
21
|
+
| "melancholy" // repeated failures, low streak
|
|
22
|
+
| "chaotic"; // many errors in short time, unpredictable
|
|
23
|
+
|
|
24
|
+
export interface MoodState {
|
|
25
|
+
current: Mood;
|
|
26
|
+
since: number; // timestamp of last mood change
|
|
27
|
+
intensity: number; // 1-3, affects how much mood modifies behavior
|
|
28
|
+
recentErrors: number; // errors in last 5 minutes
|
|
29
|
+
recentTests: number; // tests passed in last 5 minutes
|
|
30
|
+
recentDiffs: number; // large diffs in last 5 minutes
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Mood colors (for status line display) ───────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export const MOOD_COLORS: Record<Mood, string> = {
|
|
36
|
+
happy: "\ud83d\ude0a", // 😀
|
|
37
|
+
focused: "\ud83e\udd13", // 🧐
|
|
38
|
+
excited: "\ud83e\ude74", // 🥳
|
|
39
|
+
tired: "\ud83d\ude34", // 😴
|
|
40
|
+
melancholy: "\ud83d\ude14", // 😔
|
|
41
|
+
chaotic: "\ud83e\udd2f", // 🤯
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const MOOD_NAMES: Record<Mood, string> = {
|
|
45
|
+
happy: "Happy",
|
|
46
|
+
focused: "Focused",
|
|
47
|
+
excited: "Excited",
|
|
48
|
+
tired: "Tired",
|
|
49
|
+
melancholy: "Melancholy",
|
|
50
|
+
chaotic: "Chaotic",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─── Time-of-day influence ───────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export function getTimeOfDayMood(): Mood {
|
|
56
|
+
const hour = new Date().getHours();
|
|
57
|
+
if (hour >= 22 || hour < 6) return "tired"; // late night
|
|
58
|
+
if (hour >= 6 && hour < 9) return "melancholy"; // early morning
|
|
59
|
+
if (hour >= 9 && hour < 12) return "focused"; // morning
|
|
60
|
+
if (hour >= 12 && hour < 17) return "happy"; // afternoon
|
|
61
|
+
if (hour >= 17 && hour < 20) return "excited"; // evening
|
|
62
|
+
return "tired";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Mood persistence ────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const MOOD_FILE = join(buddyStateDir(), "mood.json");
|
|
68
|
+
|
|
69
|
+
function loadMoodState(): MoodState {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(MOOD_FILE, "utf8")) as MoodState;
|
|
72
|
+
} catch {
|
|
73
|
+
return {
|
|
74
|
+
current: getTimeOfDayMood(),
|
|
75
|
+
since: Date.now(),
|
|
76
|
+
intensity: 1,
|
|
77
|
+
recentErrors: 0,
|
|
78
|
+
recentTests: 0,
|
|
79
|
+
recentDiffs: 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function saveMoodState(state: MoodState): void {
|
|
85
|
+
mkdirSync(buddyStateDir(), { recursive: true });
|
|
86
|
+
writeFileSync(MOOD_FILE, JSON.stringify(state, null, 2));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Mood shift triggers ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export type MoodTrigger =
|
|
92
|
+
| "tests_pass"
|
|
93
|
+
| "tests_fail"
|
|
94
|
+
| "error"
|
|
95
|
+
| "large_diff"
|
|
96
|
+
| "achievement"
|
|
97
|
+
| "long_session"
|
|
98
|
+
| "idle"
|
|
99
|
+
| "rest";
|
|
100
|
+
|
|
101
|
+
const MOOD_TRANSITIONS: Record<Mood, Partial<Record<MoodTrigger, Mood>>> = {
|
|
102
|
+
happy: {
|
|
103
|
+
tests_pass: "excited",
|
|
104
|
+
achievement: "excited",
|
|
105
|
+
long_session: "melancholy",
|
|
106
|
+
error: "tired",
|
|
107
|
+
},
|
|
108
|
+
focused: {
|
|
109
|
+
tests_pass: "happy",
|
|
110
|
+
error: "tired",
|
|
111
|
+
long_session: "tired",
|
|
112
|
+
},
|
|
113
|
+
excited: {
|
|
114
|
+
tests_pass: "excited",
|
|
115
|
+
error: "focused",
|
|
116
|
+
idle: "happy",
|
|
117
|
+
},
|
|
118
|
+
tired: {
|
|
119
|
+
tests_pass: "focused",
|
|
120
|
+
error: "chaotic",
|
|
121
|
+
rest: "happy",
|
|
122
|
+
idle: "melancholy",
|
|
123
|
+
},
|
|
124
|
+
melancholy: {
|
|
125
|
+
tests_pass: "focused",
|
|
126
|
+
achievement: "happy",
|
|
127
|
+
error: "chaotic",
|
|
128
|
+
rest: "happy",
|
|
129
|
+
},
|
|
130
|
+
chaotic: {
|
|
131
|
+
tests_pass: "focused",
|
|
132
|
+
idle: "tired",
|
|
133
|
+
rest: "melancholy",
|
|
134
|
+
error: "chaotic",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ─── Core functions ───────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const RECENT_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
141
|
+
|
|
142
|
+
function decayRecentCounts(state: MoodState): MoodState {
|
|
143
|
+
// Decay recent counts based on time since last update
|
|
144
|
+
const elapsed = Date.now() - state.since;
|
|
145
|
+
if (elapsed > RECENT_WINDOW_MS) {
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
recentErrors: 0,
|
|
149
|
+
recentTests: 0,
|
|
150
|
+
recentDiffs: 0,
|
|
151
|
+
since: Date.now(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return state;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Shift mood based on a trigger event.
|
|
159
|
+
* Returns the new mood state.
|
|
160
|
+
*/
|
|
161
|
+
export function shiftMood(trigger: MoodTrigger): MoodState {
|
|
162
|
+
let state = loadMoodState();
|
|
163
|
+
state = decayRecentCounts(state);
|
|
164
|
+
|
|
165
|
+
// Update recent counters based on trigger
|
|
166
|
+
switch (trigger) {
|
|
167
|
+
case "error":
|
|
168
|
+
state.recentErrors += 1;
|
|
169
|
+
break;
|
|
170
|
+
case "tests_pass":
|
|
171
|
+
state.recentTests += 1;
|
|
172
|
+
break;
|
|
173
|
+
case "large_diff":
|
|
174
|
+
state.recentDiffs += 1;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for chaotic threshold (3+ errors in 5 minutes)
|
|
179
|
+
if (trigger === "error" && state.recentErrors >= 3) {
|
|
180
|
+
state.current = "chaotic";
|
|
181
|
+
state.intensity = Math.min(3, state.recentErrors - 1);
|
|
182
|
+
state.since = Date.now();
|
|
183
|
+
saveMoodState(state);
|
|
184
|
+
return state;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Look up the transition for the current mood
|
|
188
|
+
const transitions = MOOD_TRANSITIONS[state.current];
|
|
189
|
+
if (transitions && transitions[trigger]) {
|
|
190
|
+
const newMood = transitions[trigger]!;
|
|
191
|
+
if (newMood !== state.current) {
|
|
192
|
+
state.current = newMood;
|
|
193
|
+
state.intensity = Math.min(3, state.intensity + 1);
|
|
194
|
+
state.since = Date.now();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
saveMoodState(state);
|
|
199
|
+
return state;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get the current mood state.
|
|
204
|
+
*/
|
|
205
|
+
export function getMood(): MoodState {
|
|
206
|
+
return decayRecentCounts(loadMoodState());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get mood stat modifiers (±5 to effective stats based on mood).
|
|
211
|
+
* These are temporary and don't persist.
|
|
212
|
+
*/
|
|
213
|
+
export function getMoodModifier(mood: Mood, stat: StatName): number {
|
|
214
|
+
switch (mood) {
|
|
215
|
+
case "happy":
|
|
216
|
+
return stat === "WISDOM" ? +5 : 0;
|
|
217
|
+
case "focused":
|
|
218
|
+
return stat === "DEBUGGING" ? +5 : stat === "CHAOS" ? -3 : 0;
|
|
219
|
+
case "chaotic":
|
|
220
|
+
return stat === "CHAOS" ? +8 : stat === "WISDOM" ? -5 : 0;
|
|
221
|
+
case "tired":
|
|
222
|
+
return stat === "PATIENCE" ? +3 : stat === "DEBUGGING" ? -3 : 0;
|
|
223
|
+
case "melancholy":
|
|
224
|
+
return stat === "WISDOM" ? +3 : 0;
|
|
225
|
+
case "excited":
|
|
226
|
+
return stat === "CHAOS" ? +5 : stat === "PATIENCE" ? -3 : 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Mood for reactions ─────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get a mood-specific reaction suffix/prefix.
|
|
234
|
+
* Returns undefined if no mood-specific reaction should be used.
|
|
235
|
+
*/
|
|
236
|
+
export function getMoodReaction(mood: Mood): string | undefined {
|
|
237
|
+
const r = Math.random();
|
|
238
|
+
if (r > 0.3) return undefined; // 30% chance of mood reaction
|
|
239
|
+
|
|
240
|
+
switch (mood) {
|
|
241
|
+
case "happy":
|
|
242
|
+
return "*blooms*";
|
|
243
|
+
case "focused":
|
|
244
|
+
return "*intensifies*";
|
|
245
|
+
case "excited":
|
|
246
|
+
return "*vibrates with energy*";
|
|
247
|
+
case "tired":
|
|
248
|
+
return "*yawns*";
|
|
249
|
+
case "melancholy":
|
|
250
|
+
return "*sighs gently*";
|
|
251
|
+
case "chaotic":
|
|
252
|
+
return "*glitches*";
|
|
253
|
+
}
|
|
254
|
+
}
|