codeep 2.1.3 → 2.3.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/README.md +46 -2
- package/dist/acp/commands.js +57 -0
- package/dist/acp/server.js +5 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.js +27 -0
- package/dist/renderer/App.js +6 -0
- package/dist/renderer/commands.js +96 -0
- package/dist/renderer/components/Help.js +4 -0
- package/dist/renderer/components/Settings.js +10 -0
- package/dist/utils/agent.d.ts +14 -0
- package/dist/utils/agent.js +203 -10
- package/dist/utils/agentChat.d.ts +15 -0
- package/dist/utils/agentChat.js +79 -0
- package/dist/utils/agents.d.ts +57 -0
- package/dist/utils/agents.js +188 -0
- package/dist/utils/codeepCloud.d.ts +5 -0
- package/dist/utils/codeepCloud.js +58 -0
- package/dist/utils/shell.js +36 -0
- package/dist/utils/userProfile.d.ts +99 -0
- package/dist/utils/userProfile.js +351 -0
- package/package.json +1 -1
|
@@ -110,4 +110,9 @@ export declare function pullLearning(): Promise<{
|
|
|
110
110
|
} | null>;
|
|
111
111
|
export declare function pushProfiles(profiles: Record<string, object>): Promise<boolean>;
|
|
112
112
|
export declare function pullProfiles(): Promise<Record<string, object> | null>;
|
|
113
|
+
/** Push the local global profile.md to the dashboard. */
|
|
114
|
+
export declare function pushUserProfile(): Promise<boolean>;
|
|
115
|
+
/** Pull the dashboard profile.md — additive: writes only when no local profile
|
|
116
|
+
* exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
|
|
117
|
+
export declare function pullUserProfile(): Promise<number | null>;
|
|
113
118
|
export declare function syncMemoryNotes(projectName: string, notes: string[]): Promise<void>;
|
|
@@ -421,6 +421,64 @@ export async function pullProfiles() {
|
|
|
421
421
|
return null;
|
|
422
422
|
}
|
|
423
423
|
}
|
|
424
|
+
// ─── User profile sync (~/.codeep/profile.md) ──────────────────────────────────
|
|
425
|
+
//
|
|
426
|
+
// The hand-written global "About me" profile. One blob per user. Pull is
|
|
427
|
+
// additive — it writes only when no local profile.md exists, so a web edit can
|
|
428
|
+
// never clobber local work (same philosophy as the personalities/commands sync).
|
|
429
|
+
function userProfilePath() {
|
|
430
|
+
return join(homedir(), '.codeep', 'profile.md');
|
|
431
|
+
}
|
|
432
|
+
/** Push the local global profile.md to the dashboard. */
|
|
433
|
+
export async function pushUserProfile() {
|
|
434
|
+
const syncToken = getSyncToken();
|
|
435
|
+
if (!syncToken)
|
|
436
|
+
return false;
|
|
437
|
+
const path = userProfilePath();
|
|
438
|
+
if (!existsSync(path))
|
|
439
|
+
return false;
|
|
440
|
+
let content = '';
|
|
441
|
+
try {
|
|
442
|
+
content = readFileSync(path, 'utf8');
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (content.length > 32 * 1024)
|
|
448
|
+
content = content.slice(0, 32 * 1024);
|
|
449
|
+
const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
|
|
452
|
+
body: JSON.stringify({ content }),
|
|
453
|
+
});
|
|
454
|
+
return res?.ok ?? false;
|
|
455
|
+
}
|
|
456
|
+
/** Pull the dashboard profile.md — additive: writes only when no local profile
|
|
457
|
+
* exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
|
|
458
|
+
export async function pullUserProfile() {
|
|
459
|
+
const syncToken = getSyncToken();
|
|
460
|
+
if (!syncToken)
|
|
461
|
+
return null;
|
|
462
|
+
const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, { headers: { 'x-sync-token': syncToken } });
|
|
463
|
+
if (!res?.ok)
|
|
464
|
+
return null;
|
|
465
|
+
try {
|
|
466
|
+
const data = await res.json();
|
|
467
|
+
if (!data.ok || !data.content)
|
|
468
|
+
return 0;
|
|
469
|
+
const path = userProfilePath();
|
|
470
|
+
if (existsSync(path))
|
|
471
|
+
return 0; // never clobber local
|
|
472
|
+
const dir = join(homedir(), '.codeep');
|
|
473
|
+
if (!existsSync(dir))
|
|
474
|
+
mkdirSync(dir, { recursive: true });
|
|
475
|
+
writeFileSync(path, data.content);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
424
482
|
export async function syncMemoryNotes(projectName, notes) {
|
|
425
483
|
const syncToken = getSyncToken();
|
|
426
484
|
if (!syncToken)
|
package/dist/utils/shell.js
CHANGED
|
@@ -74,6 +74,37 @@ const ALLOWED_COMMANDS = new Set([
|
|
|
74
74
|
// HTTP tools
|
|
75
75
|
'http', 'https',
|
|
76
76
|
]);
|
|
77
|
+
// Interpreter flags that execute inline code straight from the command line.
|
|
78
|
+
// Without this check, a whitelisted runtime (`node`, `python`, …) becomes
|
|
79
|
+
// arbitrary code execution — `node -e "<anything>"`, `python -c "<anything>"` —
|
|
80
|
+
// bypassing the command whitelist entirely. File execution (`node app.js`)
|
|
81
|
+
// stays allowed; only the eval flags are blocked.
|
|
82
|
+
const INLINE_EVAL_SHORT = {
|
|
83
|
+
node: ['e', 'p'], bun: ['e'], python: ['c'], python3: ['c'], php: ['r'], ruby: ['e'], perl: ['e', 'E'],
|
|
84
|
+
};
|
|
85
|
+
const INLINE_EVAL_LONG = {
|
|
86
|
+
node: ['--eval', '--print'], deno: ['eval'], bun: ['--eval'],
|
|
87
|
+
};
|
|
88
|
+
function hasInlineEval(command, args) {
|
|
89
|
+
const short = INLINE_EVAL_SHORT[command] ?? [];
|
|
90
|
+
const long = INLINE_EVAL_LONG[command] ?? [];
|
|
91
|
+
if (short.length === 0 && long.length === 0)
|
|
92
|
+
return false;
|
|
93
|
+
for (const arg of args) {
|
|
94
|
+
if (arg.startsWith('--')) {
|
|
95
|
+
if (long.includes(arg.split('=')[0]))
|
|
96
|
+
return true; // --eval / --print(=...)
|
|
97
|
+
}
|
|
98
|
+
else if (arg.length > 1 && arg.startsWith('-')) {
|
|
99
|
+
if (arg.slice(1).split('').some((l) => short.includes(l)))
|
|
100
|
+
return true; // -e, -c, -pe …
|
|
101
|
+
}
|
|
102
|
+
else if (long.includes(arg)) {
|
|
103
|
+
return true; // bare subcommand, e.g. `deno eval`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
77
108
|
/**
|
|
78
109
|
* Validate if a command is safe to execute
|
|
79
110
|
*/
|
|
@@ -86,6 +117,11 @@ export function validateCommand(command, args, options) {
|
|
|
86
117
|
if (!ALLOWED_COMMANDS.has(command)) {
|
|
87
118
|
return { valid: false, reason: `Command '${command}' is not in the allowed list` };
|
|
88
119
|
}
|
|
120
|
+
// Block inline-code execution that would turn a whitelisted interpreter into
|
|
121
|
+
// arbitrary code execution (the whitelist alone doesn't stop `node -e "…"`).
|
|
122
|
+
if (hasInlineEval(command, args)) {
|
|
123
|
+
return { valid: false, reason: `Inline code execution via '${command}' (e.g. -e/-c/--eval) is not allowed in agent mode — put the code in a file and run that, or run it yourself.` };
|
|
124
|
+
}
|
|
89
125
|
// Check full command string against dangerous patterns
|
|
90
126
|
const fullCommand = `${command} ${args.join(' ')}`;
|
|
91
127
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profile — a durable, human-readable description of the user that
|
|
3
|
+
* personalises how the agent works. It is injected into the system prompt so
|
|
4
|
+
* every surface (CLI, ACP, VS Code, Zed) adapts the same way, because they all
|
|
5
|
+
* run through agent.ts.
|
|
6
|
+
*
|
|
7
|
+
* Storage (both optional, both injected when present):
|
|
8
|
+
* - **Global**: `~/.codeep/profile.md` — who the user is across
|
|
9
|
+
* all projects: preferred reply language, response style, default stack,
|
|
10
|
+
* hard "always / never" values.
|
|
11
|
+
* - **Project**: `<workspace>/.codeep/profile.md` — what THIS project is to
|
|
12
|
+
* the user: their role, goals, constraints, "don't touch" notes.
|
|
13
|
+
*
|
|
14
|
+
* Global is injected first, the project profile second, so the project context
|
|
15
|
+
* sits closest to the task and can refine/extend the global one.
|
|
16
|
+
*
|
|
17
|
+
* This file is written by the user — via `/me` or by hand. There is NO
|
|
18
|
+
* automatic extraction in this phase, so injecting it carries no surprise.
|
|
19
|
+
* Injection is gated by `config.userProfile` (default true); set it false to
|
|
20
|
+
* disable entirely.
|
|
21
|
+
*
|
|
22
|
+
* NOTE: This is distinct from the provider "profiles" feature (saved
|
|
23
|
+
* provider+model combos in config.json, see config/index.ts). Different
|
|
24
|
+
* concept, different storage, different command (`/me` vs `/profile`).
|
|
25
|
+
*/
|
|
26
|
+
/** A minimal chat message shape (avoids importing the heavier Message type). */
|
|
27
|
+
type ChatMsg = {
|
|
28
|
+
role: string;
|
|
29
|
+
content: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function globalProfilePath(): string;
|
|
32
|
+
export declare function projectProfilePath(workspaceRoot: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Auto-learned facts files. Kept SEPARATE from the user-authored profile.md so
|
|
35
|
+
* the learning pass never touches what the user wrote by hand. Populated only
|
|
36
|
+
* when `autoLearnProfile` is on (or via `/me learn`).
|
|
37
|
+
* - Global (`~/.codeep/profile.learned.md`): cross-project facts about the
|
|
38
|
+
* user (reply language, style, general stack, universal always/never).
|
|
39
|
+
* - Project (`<root>/.codeep/profile.learned.md`): facts specific to working
|
|
40
|
+
* on THIS project (role, goals, constraints, project conventions stated).
|
|
41
|
+
*/
|
|
42
|
+
export declare function globalLearnedProfilePath(): string;
|
|
43
|
+
export declare function projectLearnedProfilePath(workspaceRoot: string): string;
|
|
44
|
+
export type LearnScope = 'global' | 'project';
|
|
45
|
+
/**
|
|
46
|
+
* Build the system-prompt addendum describing the user. Returns '' when
|
|
47
|
+
* injection is disabled or when neither profile file exists. Never throws.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadUserProfilePrompt(workspaceRoot?: string): string;
|
|
50
|
+
export interface ProfileStatus {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
autoLearn: boolean;
|
|
53
|
+
globalPath: string;
|
|
54
|
+
globalExists: boolean;
|
|
55
|
+
projectPath: string | null;
|
|
56
|
+
projectExists: boolean;
|
|
57
|
+
learnedGlobalPath: string;
|
|
58
|
+
learnedGlobalExists: boolean;
|
|
59
|
+
learnedProjectPath: string | null;
|
|
60
|
+
learnedProjectExists: boolean;
|
|
61
|
+
}
|
|
62
|
+
export declare function getProfileStatus(workspaceRoot?: string): ProfileStatus;
|
|
63
|
+
/**
|
|
64
|
+
* Create a starter profile file if it doesn't exist. Never clobbers existing
|
|
65
|
+
* content. Returns the path + whether it was created, or null on failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scaffoldProfile(scope: 'global' | 'project', workspaceRoot?: string): {
|
|
68
|
+
path: string;
|
|
69
|
+
created: boolean;
|
|
70
|
+
} | null;
|
|
71
|
+
/** Render the `/me` view: injection state, file paths, and current content. */
|
|
72
|
+
export declare function formatProfileView(workspaceRoot?: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Observe a conversation and update an auto-learned profile via one cheap LLM
|
|
75
|
+
* pass that MERGES new durable facts with the existing ones (dedup, newer-wins,
|
|
76
|
+
* capped). `scope` selects the global profile (cross-project, about the person)
|
|
77
|
+
* or the project profile (this repo only; needs `workspaceRoot`). Returns the
|
|
78
|
+
* resulting facts (`updated` = whether the file changed), or null when there's
|
|
79
|
+
* nothing to learn / the call fails. Never throws.
|
|
80
|
+
*
|
|
81
|
+
* Gating (`autoLearnProfile`) is the caller's job for the automatic path;
|
|
82
|
+
* `/me learn` calls this directly on demand.
|
|
83
|
+
*/
|
|
84
|
+
export declare function updateLearnedProfile(history?: ChatMsg[], scope?: LearnScope, workspaceRoot?: string): Promise<{
|
|
85
|
+
updated: boolean;
|
|
86
|
+
facts: string;
|
|
87
|
+
} | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Auto-learn entry point for the session-save hook. No-ops unless the user
|
|
90
|
+
* opted in (`autoLearnProfile`) and enough new messages have accrued. Safe to
|
|
91
|
+
* call on every save — fire-and-forget.
|
|
92
|
+
*/
|
|
93
|
+
export declare function maybeLearnUserProfile(sessionName: string, history: ChatMsg[], workspaceRoot?: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Delete the auto-learned profile(s): always the global one, plus the project
|
|
96
|
+
* one when a workspace root is given. Returns true if any file was removed.
|
|
97
|
+
*/
|
|
98
|
+
export declare function clearLearnedProfile(workspaceRoot?: string): boolean;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profile — a durable, human-readable description of the user that
|
|
3
|
+
* personalises how the agent works. It is injected into the system prompt so
|
|
4
|
+
* every surface (CLI, ACP, VS Code, Zed) adapts the same way, because they all
|
|
5
|
+
* run through agent.ts.
|
|
6
|
+
*
|
|
7
|
+
* Storage (both optional, both injected when present):
|
|
8
|
+
* - **Global**: `~/.codeep/profile.md` — who the user is across
|
|
9
|
+
* all projects: preferred reply language, response style, default stack,
|
|
10
|
+
* hard "always / never" values.
|
|
11
|
+
* - **Project**: `<workspace>/.codeep/profile.md` — what THIS project is to
|
|
12
|
+
* the user: their role, goals, constraints, "don't touch" notes.
|
|
13
|
+
*
|
|
14
|
+
* Global is injected first, the project profile second, so the project context
|
|
15
|
+
* sits closest to the task and can refine/extend the global one.
|
|
16
|
+
*
|
|
17
|
+
* This file is written by the user — via `/me` or by hand. There is NO
|
|
18
|
+
* automatic extraction in this phase, so injecting it carries no surprise.
|
|
19
|
+
* Injection is gated by `config.userProfile` (default true); set it false to
|
|
20
|
+
* disable entirely.
|
|
21
|
+
*
|
|
22
|
+
* NOTE: This is distinct from the provider "profiles" feature (saved
|
|
23
|
+
* provider+model combos in config.json, see config/index.ts). Different
|
|
24
|
+
* concept, different storage, different command (`/me` vs `/profile`).
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
27
|
+
import { join, dirname } from 'path';
|
|
28
|
+
import { homedir } from 'os';
|
|
29
|
+
import { config } from '../config/index.js';
|
|
30
|
+
/** Max bytes read from each profile file — keeps the prompt budget bounded. */
|
|
31
|
+
const MAX_PROFILE_BYTES = 8 * 1024;
|
|
32
|
+
export function globalProfilePath() {
|
|
33
|
+
return join(homedir(), '.codeep', 'profile.md');
|
|
34
|
+
}
|
|
35
|
+
export function projectProfilePath(workspaceRoot) {
|
|
36
|
+
return join(workspaceRoot, '.codeep', 'profile.md');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Auto-learned facts files. Kept SEPARATE from the user-authored profile.md so
|
|
40
|
+
* the learning pass never touches what the user wrote by hand. Populated only
|
|
41
|
+
* when `autoLearnProfile` is on (or via `/me learn`).
|
|
42
|
+
* - Global (`~/.codeep/profile.learned.md`): cross-project facts about the
|
|
43
|
+
* user (reply language, style, general stack, universal always/never).
|
|
44
|
+
* - Project (`<root>/.codeep/profile.learned.md`): facts specific to working
|
|
45
|
+
* on THIS project (role, goals, constraints, project conventions stated).
|
|
46
|
+
*/
|
|
47
|
+
export function globalLearnedProfilePath() {
|
|
48
|
+
return join(homedir(), '.codeep', 'profile.learned.md');
|
|
49
|
+
}
|
|
50
|
+
export function projectLearnedProfilePath(workspaceRoot) {
|
|
51
|
+
return join(workspaceRoot, '.codeep', 'profile.learned.md');
|
|
52
|
+
}
|
|
53
|
+
/** Read + trim a profile file, capped. Returns '' when missing/empty/broken. */
|
|
54
|
+
function readProfile(path) {
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(path))
|
|
57
|
+
return '';
|
|
58
|
+
let content = readFileSync(path, 'utf-8');
|
|
59
|
+
if (content.length > MAX_PROFILE_BYTES)
|
|
60
|
+
content = content.slice(0, MAX_PROFILE_BYTES);
|
|
61
|
+
return content.trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the system-prompt addendum describing the user. Returns '' when
|
|
69
|
+
* injection is disabled or when neither profile file exists. Never throws.
|
|
70
|
+
*/
|
|
71
|
+
export function loadUserProfilePrompt(workspaceRoot) {
|
|
72
|
+
if (config.get('userProfile') === false)
|
|
73
|
+
return '';
|
|
74
|
+
const sections = [];
|
|
75
|
+
const global = readProfile(globalProfilePath());
|
|
76
|
+
if (global)
|
|
77
|
+
sections.push(global);
|
|
78
|
+
if (workspaceRoot) {
|
|
79
|
+
const project = readProfile(projectProfilePath(workspaceRoot));
|
|
80
|
+
if (project)
|
|
81
|
+
sections.push(project);
|
|
82
|
+
}
|
|
83
|
+
// Auto-learned facts (if any) come last — they're observations, so the
|
|
84
|
+
// hand-written profile takes visual precedence. Each file self-labels with
|
|
85
|
+
// its own heading. Global learned first, then project learned.
|
|
86
|
+
const learnedGlobal = readProfile(globalLearnedProfilePath());
|
|
87
|
+
if (learnedGlobal)
|
|
88
|
+
sections.push(learnedGlobal);
|
|
89
|
+
if (workspaceRoot) {
|
|
90
|
+
const learnedProject = readProfile(projectLearnedProfilePath(workspaceRoot));
|
|
91
|
+
if (learnedProject)
|
|
92
|
+
sections.push(learnedProject);
|
|
93
|
+
}
|
|
94
|
+
if (sections.length === 0)
|
|
95
|
+
return '';
|
|
96
|
+
return `\n\n## About the User\nThe user shared the following about themselves and how they like to work. Honor it throughout: respond in their preferred language, match their requested style, and respect their stated preferences. (Project rules in .codeep/rules.md still take precedence on any conflict.)\n\n${sections.join('\n\n---\n\n')}`;
|
|
97
|
+
}
|
|
98
|
+
export function getProfileStatus(workspaceRoot) {
|
|
99
|
+
const globalPath = globalProfilePath();
|
|
100
|
+
const projectPath = workspaceRoot ? projectProfilePath(workspaceRoot) : null;
|
|
101
|
+
const learnedGlobalPath = globalLearnedProfilePath();
|
|
102
|
+
const learnedProjectPath = workspaceRoot ? projectLearnedProfilePath(workspaceRoot) : null;
|
|
103
|
+
return {
|
|
104
|
+
enabled: config.get('userProfile') !== false,
|
|
105
|
+
autoLearn: config.get('autoLearnProfile') === true,
|
|
106
|
+
globalPath,
|
|
107
|
+
globalExists: existsSync(globalPath),
|
|
108
|
+
projectPath,
|
|
109
|
+
projectExists: projectPath ? existsSync(projectPath) : false,
|
|
110
|
+
learnedGlobalPath,
|
|
111
|
+
learnedGlobalExists: existsSync(learnedGlobalPath),
|
|
112
|
+
learnedProjectPath,
|
|
113
|
+
learnedProjectExists: learnedProjectPath ? existsSync(learnedProjectPath) : false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const GLOBAL_TEMPLATE = `# About Me
|
|
117
|
+
|
|
118
|
+
<!-- Codeep reads this file and adapts to you on every project. Write in any
|
|
119
|
+
language. Keep it short — it is added to the agent's context on each
|
|
120
|
+
request. Delete the hints you don't use. -->
|
|
121
|
+
|
|
122
|
+
## Preferences
|
|
123
|
+
- Reply language:
|
|
124
|
+
- Response style: (concise / detailed)
|
|
125
|
+
- Explain before making changes: (yes / no)
|
|
126
|
+
|
|
127
|
+
## My stack
|
|
128
|
+
- Languages:
|
|
129
|
+
- Frameworks / tools:
|
|
130
|
+
|
|
131
|
+
## Always / Never
|
|
132
|
+
- Always:
|
|
133
|
+
- Never:
|
|
134
|
+
`;
|
|
135
|
+
const PROJECT_TEMPLATE = `# About This Project (for me)
|
|
136
|
+
|
|
137
|
+
<!-- Project-specific context Codeep should know when working here. This sits
|
|
138
|
+
alongside .codeep/rules.md, which is for hard project rules. -->
|
|
139
|
+
|
|
140
|
+
## My role on this project
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## Goals
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## Constraints / don't touch
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Deploy target
|
|
150
|
+
`;
|
|
151
|
+
/**
|
|
152
|
+
* Create a starter profile file if it doesn't exist. Never clobbers existing
|
|
153
|
+
* content. Returns the path + whether it was created, or null on failure.
|
|
154
|
+
*/
|
|
155
|
+
export function scaffoldProfile(scope, workspaceRoot) {
|
|
156
|
+
let path;
|
|
157
|
+
if (scope === 'global') {
|
|
158
|
+
path = globalProfilePath();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
if (!workspaceRoot)
|
|
162
|
+
return null;
|
|
163
|
+
path = projectProfilePath(workspaceRoot);
|
|
164
|
+
}
|
|
165
|
+
if (existsSync(path))
|
|
166
|
+
return { path, created: false };
|
|
167
|
+
try {
|
|
168
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
169
|
+
writeFileSync(path, scope === 'global' ? GLOBAL_TEMPLATE : PROJECT_TEMPLATE, 'utf-8');
|
|
170
|
+
return { path, created: true };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Render the `/me` view: injection state, file paths, and current content. */
|
|
177
|
+
export function formatProfileView(workspaceRoot) {
|
|
178
|
+
const st = getProfileStatus(workspaceRoot);
|
|
179
|
+
const lines = ['## Your Codeep Profile', ''];
|
|
180
|
+
lines.push(st.enabled
|
|
181
|
+
? "**Injection:** on — your profile is added to the agent's context each request. Disable with `/me off`."
|
|
182
|
+
: '**Injection:** off — profile is saved but not used. Enable with `/me on`.');
|
|
183
|
+
lines.push(st.autoLearn
|
|
184
|
+
? '**Auto-learn:** on — Codeep updates a learned profile from your sessions. Turn off with `/me learn off`; clear with `/me forget`.'
|
|
185
|
+
: "**Auto-learn:** off — Codeep won't observe sessions. Turn on with `/me learn on`, or run `/me learn` once.");
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push('| Scope | File | Status |');
|
|
188
|
+
lines.push('|---|---|---|');
|
|
189
|
+
lines.push(`| Global | \`${st.globalPath}\` | ${st.globalExists ? 'present' : 'not created'} |`);
|
|
190
|
+
if (st.projectPath) {
|
|
191
|
+
lines.push(`| Project | \`${st.projectPath}\` | ${st.projectExists ? 'present' : 'not created'} |`);
|
|
192
|
+
}
|
|
193
|
+
lines.push(`| Learned · global (auto) | \`${st.learnedGlobalPath}\` | ${st.learnedGlobalExists ? 'present' : 'not created'} |`);
|
|
194
|
+
if (st.learnedProjectPath) {
|
|
195
|
+
lines.push(`| Learned · project (auto) | \`${st.learnedProjectPath}\` | ${st.learnedProjectExists ? 'present' : 'not created'} |`);
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
const globalRaw = readProfile(globalProfilePath());
|
|
199
|
+
const projectRaw = workspaceRoot ? readProfile(projectProfilePath(workspaceRoot)) : '';
|
|
200
|
+
const learnedGlobalRaw = readProfile(globalLearnedProfilePath());
|
|
201
|
+
const learnedProjectRaw = workspaceRoot ? readProfile(projectLearnedProfilePath(workspaceRoot)) : '';
|
|
202
|
+
if (!globalRaw && !projectRaw && !learnedGlobalRaw && !learnedProjectRaw) {
|
|
203
|
+
lines.push('No profile yet. Run `/me init` to scaffold a global template (or `/me init project` for this project), then edit the file. Or let Codeep build one for you with `/me learn on`.');
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (globalRaw) {
|
|
207
|
+
lines.push('### Global', '```md', globalRaw, '```', '');
|
|
208
|
+
}
|
|
209
|
+
if (projectRaw) {
|
|
210
|
+
lines.push('### Project', '```md', projectRaw, '```', '');
|
|
211
|
+
}
|
|
212
|
+
if (learnedGlobalRaw) {
|
|
213
|
+
lines.push('### Learned · global (auto)', '```md', learnedGlobalRaw, '```', '');
|
|
214
|
+
}
|
|
215
|
+
if (learnedProjectRaw) {
|
|
216
|
+
lines.push('### Learned · project (auto)', '```md', learnedProjectRaw, '```', '');
|
|
217
|
+
}
|
|
218
|
+
lines.push('Edit the hand-written file(s) above to update. `/me init [project]` scaffolds a starter; `/me forget` clears the learned sections.');
|
|
219
|
+
}
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
// ─── Auto-learn (Phase 2) ────────────────────────────────────────────────────
|
|
223
|
+
/** Pull clean "- " bullet lines out of an LLM response (capped at 15). */
|
|
224
|
+
function parseFactBullets(raw) {
|
|
225
|
+
return raw
|
|
226
|
+
.split('\n')
|
|
227
|
+
.map((l) => l.trim())
|
|
228
|
+
.filter((l) => l.startsWith('- ') && l.length > 2)
|
|
229
|
+
.slice(0, 15);
|
|
230
|
+
}
|
|
231
|
+
const GLOBAL_LEARN_SYSTEM = `You maintain a durable, cross-project profile of a software developer so an AI coding agent can adapt to them.
|
|
232
|
+
Given the EXISTING facts and a RECENT conversation, output an UPDATED bullet list of stable, GENERAL facts about the USER: preferred reply language, communication style, tech stack they use broadly, and universal instructions ("always/never…").
|
|
233
|
+
Rules:
|
|
234
|
+
- Keep existing facts unless the conversation clearly contradicts them (newer wins).
|
|
235
|
+
- Merge duplicates. One fact per line, each starting with "- ". Max 15 lines.
|
|
236
|
+
- ONLY durable, cross-project preferences about the person. NEVER one-off task details, file names, bug specifics, or anything tied to a single project.
|
|
237
|
+
- If the conversation reveals nothing durable, return the existing list unchanged.
|
|
238
|
+
- Output ONLY the bullet list. No preamble, no headings, no commentary.`;
|
|
239
|
+
const PROJECT_LEARN_SYSTEM = `You maintain notes about how a specific developer works on ONE particular software project, so an AI coding agent can adapt while working in this repo.
|
|
240
|
+
Given the EXISTING notes and a RECENT conversation, output an UPDATED bullet list of facts specific to THIS PROJECT: the user's role/goals here, constraints, conventions or instructions they established for this codebase, and "don't touch" areas.
|
|
241
|
+
Rules:
|
|
242
|
+
- Keep existing notes unless the conversation clearly contradicts them (newer wins).
|
|
243
|
+
- Merge duplicates. One fact per line, each starting with "- ". Max 15 lines.
|
|
244
|
+
- ONLY project-specific facts. Do NOT record generic personal preferences (reply language, code style) — those belong in the global profile.
|
|
245
|
+
- Skip transient task details (specific bugs, one-off file edits).
|
|
246
|
+
- If the conversation reveals nothing durable about the project, return the existing list unchanged.
|
|
247
|
+
- Output ONLY the bullet list. No preamble, no headings, no commentary.`;
|
|
248
|
+
/**
|
|
249
|
+
* Observe a conversation and update an auto-learned profile via one cheap LLM
|
|
250
|
+
* pass that MERGES new durable facts with the existing ones (dedup, newer-wins,
|
|
251
|
+
* capped). `scope` selects the global profile (cross-project, about the person)
|
|
252
|
+
* or the project profile (this repo only; needs `workspaceRoot`). Returns the
|
|
253
|
+
* resulting facts (`updated` = whether the file changed), or null when there's
|
|
254
|
+
* nothing to learn / the call fails. Never throws.
|
|
255
|
+
*
|
|
256
|
+
* Gating (`autoLearnProfile`) is the caller's job for the automatic path;
|
|
257
|
+
* `/me learn` calls this directly on demand.
|
|
258
|
+
*/
|
|
259
|
+
export async function updateLearnedProfile(history, scope = 'global', workspaceRoot) {
|
|
260
|
+
try {
|
|
261
|
+
const targetPath = scope === 'global'
|
|
262
|
+
? globalLearnedProfilePath()
|
|
263
|
+
: (workspaceRoot ? projectLearnedProfilePath(workspaceRoot) : null);
|
|
264
|
+
if (!targetPath)
|
|
265
|
+
return null;
|
|
266
|
+
const convo = (history || []).filter((m) => m.role === 'user' || m.role === 'assistant');
|
|
267
|
+
if (convo.length === 0)
|
|
268
|
+
return null;
|
|
269
|
+
const transcript = convo
|
|
270
|
+
.map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${String(m.content).replace(/\s+/g, ' ').slice(0, 600)}`)
|
|
271
|
+
.join('\n')
|
|
272
|
+
.slice(0, 24000);
|
|
273
|
+
const existingFacts = parseFactBullets(readProfile(targetPath)).join('\n');
|
|
274
|
+
const system = scope === 'global' ? GLOBAL_LEARN_SYSTEM : PROJECT_LEARN_SYSTEM;
|
|
275
|
+
const user = `EXISTING FACTS:\n${existingFacts || '(none yet)'}\n\nRECENT CONVERSATION:\n${transcript}`;
|
|
276
|
+
const { chat } = await import('../api/index.js');
|
|
277
|
+
const raw = (await chat(user, [{ role: 'system', content: system }])).trim();
|
|
278
|
+
const bullets = parseFactBullets(raw);
|
|
279
|
+
if (bullets.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
const facts = bullets.join('\n');
|
|
282
|
+
if (facts === existingFacts)
|
|
283
|
+
return { updated: false, facts };
|
|
284
|
+
const heading = scope === 'global'
|
|
285
|
+
? 'What Codeep has learned about me'
|
|
286
|
+
: 'What Codeep has learned about this project';
|
|
287
|
+
const content = `# ${heading}\n\n<!-- Auto-observed from your Codeep sessions. Edit freely, clear with \`/me forget\`, or turn off with \`/me learn off\`. -->\n\n${facts}\n`;
|
|
288
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
289
|
+
writeFileSync(targetPath, content, 'utf-8');
|
|
290
|
+
return { updated: true, facts };
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Throttle so the 5s autosave cadence doesn't spawn an LLM call every tick:
|
|
297
|
+
// only learn once the session grew by a few messages, one in-flight per session.
|
|
298
|
+
const learnedAtMsgCount = new Map();
|
|
299
|
+
const learnInFlight = new Set();
|
|
300
|
+
const LEARN_MIN_NEW_MESSAGES = 6;
|
|
301
|
+
/**
|
|
302
|
+
* Auto-learn entry point for the session-save hook. No-ops unless the user
|
|
303
|
+
* opted in (`autoLearnProfile`) and enough new messages have accrued. Safe to
|
|
304
|
+
* call on every save — fire-and-forget.
|
|
305
|
+
*/
|
|
306
|
+
export async function maybeLearnUserProfile(sessionName, history, workspaceRoot) {
|
|
307
|
+
if (config.get('autoLearnProfile') !== true)
|
|
308
|
+
return;
|
|
309
|
+
if (config.get('userProfile') === false)
|
|
310
|
+
return;
|
|
311
|
+
if (learnInFlight.has(sessionName))
|
|
312
|
+
return;
|
|
313
|
+
const count = (history || []).filter((m) => m.role === 'user' || m.role === 'assistant').length;
|
|
314
|
+
if (count - (learnedAtMsgCount.get(sessionName) ?? 0) < LEARN_MIN_NEW_MESSAGES)
|
|
315
|
+
return;
|
|
316
|
+
learnInFlight.add(sessionName);
|
|
317
|
+
try {
|
|
318
|
+
await updateLearnedProfile(history, 'global');
|
|
319
|
+
if (workspaceRoot)
|
|
320
|
+
await updateLearnedProfile(history, 'project', workspaceRoot);
|
|
321
|
+
learnedAtMsgCount.set(sessionName, count);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* never block a session save */
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
learnInFlight.delete(sessionName);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Delete the auto-learned profile(s): always the global one, plus the project
|
|
332
|
+
* one when a workspace root is given. Returns true if any file was removed.
|
|
333
|
+
*/
|
|
334
|
+
export function clearLearnedProfile(workspaceRoot) {
|
|
335
|
+
let removed = false;
|
|
336
|
+
const paths = [
|
|
337
|
+
globalLearnedProfilePath(),
|
|
338
|
+
workspaceRoot ? projectLearnedProfilePath(workspaceRoot) : null,
|
|
339
|
+
];
|
|
340
|
+
for (const p of paths) {
|
|
341
|
+
if (p && existsSync(p)) {
|
|
342
|
+
try {
|
|
343
|
+
rmSync(p);
|
|
344
|
+
removed = true;
|
|
345
|
+
}
|
|
346
|
+
catch { /* ignore */ }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
learnedAtMsgCount.clear();
|
|
350
|
+
return removed;
|
|
351
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|