ccprofiles-core 0.12.0 → 0.13.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/dist/claudemem.d.ts +68 -0
- package/dist/claudemem.js +169 -0
- package/dist/discovery.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which credentials claude-mem's observer runs on.
|
|
3
|
+
*
|
|
4
|
+
* claude-mem's worker is one process shared by every profile. When it spawns its observer
|
|
5
|
+
* `claude`, it STRIPS ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL and
|
|
6
|
+
* CLAUDE_CONFIG_DIR from the env it inherited, then re-adds credentials from exactly two places:
|
|
7
|
+
* 1. ~/.claude-mem/.env (those same three keys) — wins if present
|
|
8
|
+
* 2. the OAuth token of CLAUDE_MEM_CLAUDE_CONFIG_DIR (default ~/.claude), read from the
|
|
9
|
+
* keychain under the same per-dir service name Claude Code uses
|
|
10
|
+
* So a user whose only working account is a token/API-key profile gets no credentials at all,
|
|
11
|
+
* and claude-mem silently stops recording. We point it at an OAuth profile when one exists,
|
|
12
|
+
* and otherwise copy a key profile's credentials into a managed block of its .env.
|
|
13
|
+
* (Verified against claude-mem 13.39.0 worker-service.cjs: kGe/Eh/lre.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLAUDE_MEM_BEGIN = "# >>> ccprofiles managed >>>";
|
|
16
|
+
export declare const CLAUDE_MEM_END = "# <<< ccprofiles managed <<<";
|
|
17
|
+
export interface ClaudeMemCandidate {
|
|
18
|
+
name: string;
|
|
19
|
+
dir: string;
|
|
20
|
+
/** logged in with OAuth (`oauthAccount` in .claude.json) */
|
|
21
|
+
oauth: boolean;
|
|
22
|
+
/** the profile's provider env (settings.json env + launcher env); values may be secret:// refs */
|
|
23
|
+
env: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export type ClaudeMemAuth = {
|
|
26
|
+
kind: 'oauth';
|
|
27
|
+
profile: string;
|
|
28
|
+
configDir: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'api-key' | 'gateway';
|
|
31
|
+
profile: string;
|
|
32
|
+
env: Record<string, string>;
|
|
33
|
+
model?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* OAuth beats a key; a direct Anthropic key beats a gateway. Within a tier, the profile already in
|
|
37
|
+
* use stays (no churn between runs), then the default ~/.claude, then name order.
|
|
38
|
+
*/
|
|
39
|
+
export declare function pickClaudeMemAuth(cands: ClaudeMemCandidate[], current?: {
|
|
40
|
+
configDir?: string;
|
|
41
|
+
profile?: string;
|
|
42
|
+
}): ClaudeMemAuth | null;
|
|
43
|
+
export interface ClaudeMemFiles {
|
|
44
|
+
/** parsed ~/.claude-mem/settings.json, or null when absent */
|
|
45
|
+
settings: Record<string, unknown> | null;
|
|
46
|
+
/** raw ~/.claude-mem/.env, '' when absent */
|
|
47
|
+
envText: string;
|
|
48
|
+
}
|
|
49
|
+
/** The profile the managed block was last written from — keeps the choice stable across runs. */
|
|
50
|
+
export declare function claudeMemManagedProfile(envText: string): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Desired settings.json + .env for `auth`, or `skip` when the user configured claude-mem by hand
|
|
53
|
+
* (another provider, or their own keys in .env) — those are never overwritten. `auth` env values
|
|
54
|
+
* must already be resolved (no secret:// refs).
|
|
55
|
+
*/
|
|
56
|
+
export declare function renderClaudeMemConfig(files: ClaudeMemFiles, auth: ClaudeMemAuth, defaultConfigDir: string): {
|
|
57
|
+
skip: string;
|
|
58
|
+
} | {
|
|
59
|
+
settings: Record<string, unknown>;
|
|
60
|
+
envText: string;
|
|
61
|
+
changes: string[];
|
|
62
|
+
};
|
|
63
|
+
/** Read claude-mem's config. A corrupt settings.json throws — rewriting it would wipe every other key. */
|
|
64
|
+
export declare function readClaudeMemFiles(dataDir: string): Promise<ClaudeMemFiles>;
|
|
65
|
+
export declare function writeClaudeMemFiles(dataDir: string, before: ClaudeMemFiles, after: {
|
|
66
|
+
settings: Record<string, unknown>;
|
|
67
|
+
envText: string;
|
|
68
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { atomicWrite } from './fsutil.js';
|
|
4
|
+
/**
|
|
5
|
+
* Which credentials claude-mem's observer runs on.
|
|
6
|
+
*
|
|
7
|
+
* claude-mem's worker is one process shared by every profile. When it spawns its observer
|
|
8
|
+
* `claude`, it STRIPS ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL and
|
|
9
|
+
* CLAUDE_CONFIG_DIR from the env it inherited, then re-adds credentials from exactly two places:
|
|
10
|
+
* 1. ~/.claude-mem/.env (those same three keys) — wins if present
|
|
11
|
+
* 2. the OAuth token of CLAUDE_MEM_CLAUDE_CONFIG_DIR (default ~/.claude), read from the
|
|
12
|
+
* keychain under the same per-dir service name Claude Code uses
|
|
13
|
+
* So a user whose only working account is a token/API-key profile gets no credentials at all,
|
|
14
|
+
* and claude-mem silently stops recording. We point it at an OAuth profile when one exists,
|
|
15
|
+
* and otherwise copy a key profile's credentials into a managed block of its .env.
|
|
16
|
+
* (Verified against claude-mem 13.39.0 worker-service.cjs: kGe/Eh/lre.)
|
|
17
|
+
*/
|
|
18
|
+
export const CLAUDE_MEM_BEGIN = '# >>> ccprofiles managed >>>';
|
|
19
|
+
export const CLAUDE_MEM_END = '# <<< ccprofiles managed <<<';
|
|
20
|
+
const CRED_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'];
|
|
21
|
+
// a gateway (z.ai, kimi, ...) rejects claude-mem's default Claude model id; these are where a
|
|
22
|
+
// profile declares which model the gateway actually serves for small/fast work
|
|
23
|
+
const MODEL_KEYS = ['ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL', 'ANTHROPIC_MODEL'];
|
|
24
|
+
const hasKey = (env) => !!(env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN);
|
|
25
|
+
/**
|
|
26
|
+
* OAuth beats a key; a direct Anthropic key beats a gateway. Within a tier, the profile already in
|
|
27
|
+
* use stays (no churn between runs), then the default ~/.claude, then name order.
|
|
28
|
+
*/
|
|
29
|
+
export function pickClaudeMemAuth(cands, current = {}) {
|
|
30
|
+
const rank = (xs, isCurrent) => xs.find(isCurrent) ?? xs.find(c => c.dir.endsWith('/.claude') || c.dir.endsWith('\\.claude')) ?? xs[0];
|
|
31
|
+
// a profile that sets a key or base URL runs on THAT, not its OAuth login
|
|
32
|
+
const oauth = cands.filter(c => c.oauth && !hasKey(c.env) && !c.env.ANTHROPIC_BASE_URL);
|
|
33
|
+
if (oauth.length) {
|
|
34
|
+
const c = rank(oauth, c => c.dir === current.configDir);
|
|
35
|
+
return { kind: 'oauth', profile: c.name, configDir: c.dir };
|
|
36
|
+
}
|
|
37
|
+
const keyed = cands.filter(c => hasKey(c.env));
|
|
38
|
+
const direct = keyed.filter(c => !c.env.ANTHROPIC_BASE_URL);
|
|
39
|
+
const pool = direct.length ? direct : keyed;
|
|
40
|
+
if (!pool.length)
|
|
41
|
+
return null;
|
|
42
|
+
const c = rank(pool, c => c.name === current.profile);
|
|
43
|
+
const env = {};
|
|
44
|
+
for (const k of CRED_KEYS)
|
|
45
|
+
if (c.env[k])
|
|
46
|
+
env[k] = c.env[k];
|
|
47
|
+
if (!c.env.ANTHROPIC_BASE_URL)
|
|
48
|
+
return { kind: 'api-key', profile: c.name, env };
|
|
49
|
+
const model = MODEL_KEYS.map(k => c.env[k]).find(Boolean);
|
|
50
|
+
return { kind: 'gateway', profile: c.name, env, ...(model ? { model } : {}) };
|
|
51
|
+
}
|
|
52
|
+
/** Split .env into the managed block's lines and everything else. */
|
|
53
|
+
function splitEnv(text) {
|
|
54
|
+
const outside = [], block = [];
|
|
55
|
+
let inBlock = false;
|
|
56
|
+
for (const line of text.split('\n')) {
|
|
57
|
+
if (line === CLAUDE_MEM_BEGIN) {
|
|
58
|
+
inBlock = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (line === CLAUDE_MEM_END) {
|
|
62
|
+
inBlock = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
;
|
|
66
|
+
(inBlock ? block : outside).push(line);
|
|
67
|
+
}
|
|
68
|
+
while (outside.length && outside[outside.length - 1] === '')
|
|
69
|
+
outside.pop();
|
|
70
|
+
return { outside, block };
|
|
71
|
+
}
|
|
72
|
+
const blockMeta = (block, key) => block.find(l => l.startsWith(`# ${key}=`))?.slice(`# ${key}=`.length);
|
|
73
|
+
/** The profile the managed block was last written from — keeps the choice stable across runs. */
|
|
74
|
+
export function claudeMemManagedProfile(envText) {
|
|
75
|
+
return blockMeta(splitEnv(envText).block, 'profile');
|
|
76
|
+
}
|
|
77
|
+
function envLine(k, v) {
|
|
78
|
+
// node's parseEnv (what claude-mem reads with) has its own quoting rules; refusing anything
|
|
79
|
+
// that would need quoting is simpler than matching them, and real tokens/URLs never do
|
|
80
|
+
if (!/^[^\s"'`#\\]+$/.test(v))
|
|
81
|
+
throw new Error(`${k} has characters claude-mem's .env cannot hold safely`);
|
|
82
|
+
return `${k}=${v}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Desired settings.json + .env for `auth`, or `skip` when the user configured claude-mem by hand
|
|
86
|
+
* (another provider, or their own keys in .env) — those are never overwritten. `auth` env values
|
|
87
|
+
* must already be resolved (no secret:// refs).
|
|
88
|
+
*/
|
|
89
|
+
export function renderClaudeMemConfig(files, auth, defaultConfigDir) {
|
|
90
|
+
const settings = { ...(files.settings ?? {}) };
|
|
91
|
+
const provider = settings.CLAUDE_MEM_PROVIDER;
|
|
92
|
+
if (typeof provider === 'string' && provider && provider !== 'claude')
|
|
93
|
+
return { skip: `claude-mem uses provider "${provider}" — leaving its auth alone` };
|
|
94
|
+
const { outside, block } = splitEnv(files.envText);
|
|
95
|
+
if (outside.some(l => CRED_KEYS.some(k => l.trimStart().replace(/^export\s+/, '').startsWith(`${k}=`))))
|
|
96
|
+
return { skip: 'claude-mem has its own credentials in ~/.claude-mem/.env — leaving them alone' };
|
|
97
|
+
const changes = [];
|
|
98
|
+
const set = (k, v) => {
|
|
99
|
+
if ((settings[k] ?? '') !== v) {
|
|
100
|
+
settings[k] = v;
|
|
101
|
+
changes.push(`${k}=${v || '(default)'}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const prevModel = blockMeta(block, 'model-before');
|
|
105
|
+
let newBlock = [];
|
|
106
|
+
if (auth.kind === 'oauth') {
|
|
107
|
+
set('CLAUDE_MEM_CLAUDE_AUTH_METHOD', 'subscription');
|
|
108
|
+
// '' means ~/.claude — claude-mem treats the default dir as non-explicit either way
|
|
109
|
+
set('CLAUDE_MEM_CLAUDE_CONFIG_DIR', auth.configDir === defaultConfigDir ? '' : auth.configDir);
|
|
110
|
+
if (prevModel !== undefined)
|
|
111
|
+
set('CLAUDE_MEM_MODEL', prevModel);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
set('CLAUDE_MEM_CLAUDE_AUTH_METHOD', auth.kind);
|
|
115
|
+
set('CLAUDE_MEM_CLAUDE_CONFIG_DIR', '');
|
|
116
|
+
newBlock.push(`# profile=${auth.profile}`);
|
|
117
|
+
if (auth.kind === 'gateway' && auth.model) {
|
|
118
|
+
// remember the model we replaced, once, so switching back to Anthropic restores it
|
|
119
|
+
const before = prevModel ?? (typeof settings.CLAUDE_MEM_MODEL === 'string' ? settings.CLAUDE_MEM_MODEL : '');
|
|
120
|
+
newBlock.push(`# model-before=${before}`);
|
|
121
|
+
set('CLAUDE_MEM_MODEL', auth.model);
|
|
122
|
+
}
|
|
123
|
+
else if (prevModel !== undefined)
|
|
124
|
+
set('CLAUDE_MEM_MODEL', prevModel);
|
|
125
|
+
for (const [k, v] of Object.entries(auth.env))
|
|
126
|
+
newBlock.push(envLine(k, v));
|
|
127
|
+
}
|
|
128
|
+
const blockChanged = newBlock.join('\n') !== block.join('\n');
|
|
129
|
+
if (blockChanged)
|
|
130
|
+
changes.push(newBlock.length ? `.env credentials from profile "${auth.profile}"` : '.env credentials removed');
|
|
131
|
+
const envText = newBlock.length
|
|
132
|
+
? [...outside, ...(outside.length ? [''] : []), CLAUDE_MEM_BEGIN, ...newBlock, CLAUDE_MEM_END, ''].join('\n')
|
|
133
|
+
: outside.length ? outside.join('\n') + '\n' : '';
|
|
134
|
+
return { settings, envText, changes };
|
|
135
|
+
}
|
|
136
|
+
/** Read claude-mem's config. A corrupt settings.json throws — rewriting it would wipe every other key. */
|
|
137
|
+
export async function readClaudeMemFiles(dataDir) {
|
|
138
|
+
let settings = null;
|
|
139
|
+
try {
|
|
140
|
+
settings = JSON.parse(await readFile(join(dataDir, 'settings.json'), 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e?.code !== 'ENOENT')
|
|
144
|
+
throw new Error(`cannot read ${join(dataDir, 'settings.json')}: ${e.message} — fix or remove it`);
|
|
145
|
+
}
|
|
146
|
+
let envText = '';
|
|
147
|
+
try {
|
|
148
|
+
envText = await readFile(join(dataDir, '.env'), 'utf8');
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (e?.code !== 'ENOENT')
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
return { settings, envText };
|
|
155
|
+
}
|
|
156
|
+
export async function writeClaudeMemFiles(dataDir, before, after) {
|
|
157
|
+
const settingsPath = join(dataDir, 'settings.json');
|
|
158
|
+
if (JSON.stringify(after.settings) !== JSON.stringify(before.settings ?? {})) {
|
|
159
|
+
let mode = 0o600;
|
|
160
|
+
try {
|
|
161
|
+
mode = (await stat(settingsPath)).mode & 0o777;
|
|
162
|
+
}
|
|
163
|
+
catch { /* new file */ }
|
|
164
|
+
await atomicWrite(settingsPath, JSON.stringify(after.settings, null, 2) + '\n', { mode });
|
|
165
|
+
}
|
|
166
|
+
// .env holds a live API key — always owner-only
|
|
167
|
+
if (after.envText !== before.envText)
|
|
168
|
+
await atomicWrite(join(dataDir, '.env'), after.envText, { mode: 0o600 });
|
|
169
|
+
}
|
package/dist/discovery.js
CHANGED
|
@@ -31,8 +31,11 @@ function userScopeEntries(entries) {
|
|
|
31
31
|
export function readInstalledPluginVersions(plugins) {
|
|
32
32
|
const out = {};
|
|
33
33
|
for (const [id, entries] of Object.entries(plugins)) {
|
|
34
|
-
for (const { version } of userScopeEntries(entries)) {
|
|
35
|
-
|
|
34
|
+
for (const { version, gitCommitSha } of userScopeEntries(entries)) {
|
|
35
|
+
// Newer Claude Code writes the short sha INTO `version` instead of 'unknown' — same unusable
|
|
36
|
+
// identity, so it must be skipped the same way.
|
|
37
|
+
const isSha = typeof gitCommitSha === 'string' && typeof version === 'string' && gitCommitSha.startsWith(version);
|
|
38
|
+
if (typeof version === 'string' && version && version !== 'unknown' && !isSha) {
|
|
36
39
|
out[id] = version;
|
|
37
40
|
break;
|
|
38
41
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED