@memoarchitect/tools 0.6.2 → 0.6.4
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 +2 -2
- package/package.json +2 -2
- package/packages/tools/lib/bin/memo.js +39 -2
- package/packages/tools/lib/bin/memo.js.map +1 -1
- package/packages/tools/lib/browser/index.d.ts +3 -0
- package/packages/tools/lib/browser/index.d.ts.map +1 -1
- package/packages/tools/lib/commands/init.d.ts +3 -0
- package/packages/tools/lib/commands/init.d.ts.map +1 -1
- package/packages/tools/lib/commands/init.js +74 -17
- package/packages/tools/lib/commands/init.js.map +1 -1
- package/packages/tools/lib/commands/install.js +1 -1
- package/packages/tools/lib/commands/install.js.map +1 -1
- package/packages/tools/lib/commands/mcp.d.ts +15 -0
- package/packages/tools/lib/commands/mcp.d.ts.map +1 -0
- package/packages/tools/lib/commands/mcp.js +128 -0
- package/packages/tools/lib/commands/mcp.js.map +1 -0
- package/packages/tools/lib/index.d.ts.map +1 -1
- package/packages/tools/lib/index.js +4 -0
- package/packages/tools/lib/index.js.map +1 -1
- package/packages/tools/lib/llm/chat-engine.d.ts +97 -0
- package/packages/tools/lib/llm/chat-engine.d.ts.map +1 -0
- package/packages/tools/lib/llm/chat-engine.js +463 -0
- package/packages/tools/lib/llm/chat-engine.js.map +1 -0
- package/packages/tools/lib/llm/index.d.ts +2 -0
- package/packages/tools/lib/llm/index.d.ts.map +1 -1
- package/packages/tools/lib/llm/index.js +2 -0
- package/packages/tools/lib/llm/index.js.map +1 -1
- package/packages/tools/lib/llm/llm-provider.d.ts +40 -4
- package/packages/tools/lib/llm/llm-provider.d.ts.map +1 -1
- package/packages/tools/lib/llm/llm-provider.js +180 -37
- package/packages/tools/lib/llm/llm-provider.js.map +1 -1
- package/packages/tools/lib/llm/llm-settings.d.ts +84 -0
- package/packages/tools/lib/llm/llm-settings.d.ts.map +1 -0
- package/packages/tools/lib/llm/llm-settings.js +249 -0
- package/packages/tools/lib/llm/llm-settings.js.map +1 -0
- package/packages/tools/lib/mcp/index.d.ts +4 -0
- package/packages/tools/lib/mcp/index.d.ts.map +1 -0
- package/packages/tools/lib/mcp/index.js +9 -0
- package/packages/tools/lib/mcp/index.js.map +1 -0
- package/packages/tools/lib/mcp/model-loader.d.ts +11 -0
- package/packages/tools/lib/mcp/model-loader.d.ts.map +1 -0
- package/packages/tools/lib/mcp/model-loader.js +87 -0
- package/packages/tools/lib/mcp/model-loader.js.map +1 -0
- package/packages/tools/lib/mcp/server.d.ts +8 -0
- package/packages/tools/lib/mcp/server.d.ts.map +1 -0
- package/packages/tools/lib/mcp/server.js +132 -0
- package/packages/tools/lib/mcp/server.js.map +1 -0
- package/packages/tools/lib/mcp/tools.d.ts +18 -0
- package/packages/tools/lib/mcp/tools.d.ts.map +1 -0
- package/packages/tools/lib/mcp/tools.js +220 -0
- package/packages/tools/lib/mcp/tools.js.map +1 -0
- package/packages/tools/lib/model/builder.js +1 -1
- package/packages/tools/lib/model/builder.js.map +1 -1
- package/packages/tools/lib/model/content-store.d.ts.map +1 -1
- package/packages/tools/lib/model/content-store.js +3 -4
- package/packages/tools/lib/model/content-store.js.map +1 -1
- package/packages/tools/lib/model/view-deriver.d.ts +3 -2
- package/packages/tools/lib/model/view-deriver.d.ts.map +1 -1
- package/packages/tools/lib/model/view-deriver.js +33 -10
- package/packages/tools/lib/model/view-deriver.js.map +1 -1
- package/packages/tools/lib/protocol/messages.d.ts +88 -2
- package/packages/tools/lib/protocol/messages.d.ts.map +1 -1
- package/packages/tools/lib/server/dev-server.d.ts.map +1 -1
- package/packages/tools/lib/server/dev-server.js +194 -10
- package/packages/tools/lib/server/dev-server.js.map +1 -1
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
// ─── LLM Provider Abstraction ────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
3
|
// Provider-agnostic interface for LLM calls. Supports OpenAI-compatible APIs
|
|
4
|
-
// (OpenAI, Azure, local models) and Anthropic Claude
|
|
4
|
+
// (OpenAI, Azure, local models) and Anthropic Claude, including tool calling.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately provider-neutral and SDK-free: both providers are driven over
|
|
7
|
+
// native fetch so the package stays dependency-light and neither vendor's SDK
|
|
8
|
+
// dictates the message shape. Messages and tool calls use MEMO's own types and
|
|
9
|
+
// are translated to each wire format at the edge.
|
|
5
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
11
|
+
import { resolveLlmSettings, DEFAULT_MODELS } from './llm-settings.js';
|
|
12
|
+
/**
|
|
13
|
+
* Resolve LLM config from the environment, a project `.env`, project settings,
|
|
14
|
+
* and stored user credentials — see `llm-settings.ts` for the precedence order.
|
|
15
|
+
*
|
|
16
|
+
* `projectRoot` is optional so existing callers that only ever used real
|
|
17
|
+
* environment variables keep working unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveLLMConfig(projectRoot) {
|
|
20
|
+
const settings = resolveLlmSettings(projectRoot);
|
|
21
|
+
if (!settings.configured || !settings.apiKey || !settings.provider)
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
provider: settings.provider,
|
|
25
|
+
apiKey: settings.apiKey,
|
|
26
|
+
model: settings.model,
|
|
27
|
+
baseUrl: settings.baseUrl,
|
|
28
|
+
};
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* Create an LLM provider from config.
|
|
@@ -35,31 +38,79 @@ export function createProvider(config) {
|
|
|
35
38
|
return createOpenAIProvider(config);
|
|
36
39
|
}
|
|
37
40
|
// ─── OpenAI-compatible provider ─────────────────────────────────────────────
|
|
41
|
+
function toOpenAIMessages(messages) {
|
|
42
|
+
return messages.map(m => {
|
|
43
|
+
if (m.role === 'tool') {
|
|
44
|
+
return { role: 'tool', tool_call_id: m.toolCallId, content: m.content };
|
|
45
|
+
}
|
|
46
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
47
|
+
return {
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
content: m.content || null,
|
|
50
|
+
tool_calls: m.toolCalls.map(c => ({
|
|
51
|
+
id: c.id,
|
|
52
|
+
type: 'function',
|
|
53
|
+
function: { name: c.name, arguments: JSON.stringify(c.input) },
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { role: m.role, content: m.content };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function openAIStopReason(finish, hasToolCalls) {
|
|
61
|
+
if (hasToolCalls || finish === 'tool_calls')
|
|
62
|
+
return 'tool_use';
|
|
63
|
+
if (finish === 'stop')
|
|
64
|
+
return 'end_turn';
|
|
65
|
+
if (finish === 'length')
|
|
66
|
+
return 'max_tokens';
|
|
67
|
+
if (finish === 'content_filter')
|
|
68
|
+
return 'refusal';
|
|
69
|
+
return 'other';
|
|
70
|
+
}
|
|
38
71
|
function createOpenAIProvider(config) {
|
|
39
72
|
const baseUrl = config.baseUrl || 'https://api.openai.com/v1';
|
|
40
73
|
return {
|
|
41
74
|
name: `openai/${config.model}`,
|
|
42
75
|
async complete(options) {
|
|
76
|
+
const body = {
|
|
77
|
+
model: config.model,
|
|
78
|
+
messages: toOpenAIMessages(options.messages),
|
|
79
|
+
temperature: options.temperature ?? 0.3,
|
|
80
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
81
|
+
};
|
|
82
|
+
if (options.tools?.length) {
|
|
83
|
+
body.tools = options.tools.map(t => ({
|
|
84
|
+
type: 'function',
|
|
85
|
+
function: { name: t.name, description: t.description, parameters: t.inputSchema },
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
43
88
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
44
89
|
method: 'POST',
|
|
45
90
|
headers: {
|
|
46
91
|
'Content-Type': 'application/json',
|
|
47
92
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
48
93
|
},
|
|
49
|
-
body: JSON.stringify(
|
|
50
|
-
model: config.model,
|
|
51
|
-
messages: options.messages,
|
|
52
|
-
temperature: options.temperature ?? 0.3,
|
|
53
|
-
max_tokens: options.maxTokens ?? 4096,
|
|
54
|
-
}),
|
|
94
|
+
body: JSON.stringify(body),
|
|
55
95
|
});
|
|
56
96
|
if (!response.ok) {
|
|
57
97
|
const text = await response.text();
|
|
58
98
|
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
59
99
|
}
|
|
60
100
|
const data = await response.json();
|
|
101
|
+
const choice = data.choices?.[0];
|
|
102
|
+
const message = choice?.message ?? {};
|
|
103
|
+
const toolCalls = (message.tool_calls ?? [])
|
|
104
|
+
.filter((c) => c?.function?.name)
|
|
105
|
+
.map((c) => ({
|
|
106
|
+
id: c.id,
|
|
107
|
+
name: c.function.name,
|
|
108
|
+
input: safeParseJson(c.function.arguments),
|
|
109
|
+
}));
|
|
61
110
|
return {
|
|
62
|
-
content:
|
|
111
|
+
content: message.content ?? '',
|
|
112
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
113
|
+
stopReason: openAIStopReason(choice?.finish_reason, toolCalls.length > 0),
|
|
63
114
|
usage: data.usage ? {
|
|
64
115
|
promptTokens: data.usage.prompt_tokens,
|
|
65
116
|
completionTokens: data.usage.completion_tokens,
|
|
@@ -69,26 +120,111 @@ function createOpenAIProvider(config) {
|
|
|
69
120
|
},
|
|
70
121
|
};
|
|
71
122
|
}
|
|
123
|
+
/** Tool arguments arrive as a JSON string; a malformed one must not crash the loop. */
|
|
124
|
+
function safeParseJson(raw) {
|
|
125
|
+
if (typeof raw !== 'string')
|
|
126
|
+
return raw ?? {};
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
72
135
|
// ─── Anthropic provider ─────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Current-generation Claude models reject `temperature` / `top_p` / `top_k`
|
|
138
|
+
* with a 400 — the sampling parameters were removed starting with Opus 4.7.
|
|
139
|
+
* Sending the old default of 0.3 to any of them fails the request outright,
|
|
140
|
+
* so the parameter is only included for models known to still accept it.
|
|
141
|
+
*/
|
|
142
|
+
const MODELS_REJECTING_SAMPLING_PARAMS = [
|
|
143
|
+
'claude-opus-5',
|
|
144
|
+
'claude-opus-4-7',
|
|
145
|
+
'claude-opus-4-8',
|
|
146
|
+
'claude-sonnet-5',
|
|
147
|
+
'claude-fable-5',
|
|
148
|
+
'claude-mythos-5',
|
|
149
|
+
];
|
|
150
|
+
export function acceptsSamplingParams(model) {
|
|
151
|
+
return !MODELS_REJECTING_SAMPLING_PARAMS.some(prefix => model.startsWith(prefix));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Translate MEMO messages into Anthropic's content-block format.
|
|
155
|
+
*
|
|
156
|
+
* Anthropic has no `tool` role: results are `tool_result` blocks inside a user
|
|
157
|
+
* message, and consecutive results must be merged into one message so parallel
|
|
158
|
+
* tool calls are answered in a single turn.
|
|
159
|
+
*/
|
|
160
|
+
function toAnthropicMessages(messages) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const m of messages) {
|
|
163
|
+
if (m.role === 'system')
|
|
164
|
+
continue; // hoisted to the top-level `system` field
|
|
165
|
+
if (m.role === 'tool') {
|
|
166
|
+
const block = {
|
|
167
|
+
type: 'tool_result',
|
|
168
|
+
tool_use_id: m.toolCallId,
|
|
169
|
+
content: m.content,
|
|
170
|
+
...(m.isError ? { is_error: true } : {}),
|
|
171
|
+
};
|
|
172
|
+
const last = out[out.length - 1];
|
|
173
|
+
if (last?.role === 'user' && last.content.every(b => b.type === 'tool_result')) {
|
|
174
|
+
last.content.push(block);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
out.push({ role: 'user', content: [block] });
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
182
|
+
const content = [];
|
|
183
|
+
if (m.content)
|
|
184
|
+
content.push({ type: 'text', text: m.content });
|
|
185
|
+
for (const c of m.toolCalls) {
|
|
186
|
+
content.push({ type: 'tool_use', id: c.id, name: c.name, input: c.input });
|
|
187
|
+
}
|
|
188
|
+
out.push({ role: 'assistant', content });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
out.push({ role: m.role, content: [{ type: 'text', text: m.content }] });
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function anthropicStopReason(raw) {
|
|
196
|
+
switch (raw) {
|
|
197
|
+
case 'tool_use': return 'tool_use';
|
|
198
|
+
case 'end_turn': return 'end_turn';
|
|
199
|
+
case 'max_tokens': return 'max_tokens';
|
|
200
|
+
case 'refusal': return 'refusal';
|
|
201
|
+
default: return 'other';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
73
204
|
function createAnthropicProvider(config) {
|
|
74
205
|
const baseUrl = config.baseUrl || 'https://api.anthropic.com';
|
|
75
206
|
return {
|
|
76
207
|
name: `anthropic/${config.model}`,
|
|
77
208
|
async complete(options) {
|
|
78
|
-
// Separate system message from user/assistant messages
|
|
79
209
|
const systemMsg = options.messages.find(m => m.role === 'system');
|
|
80
|
-
const chatMessages = options.messages
|
|
81
|
-
.filter(m => m.role !== 'system')
|
|
82
|
-
.map(m => ({ role: m.role, content: m.content }));
|
|
83
210
|
const body = {
|
|
84
211
|
model: config.model,
|
|
85
|
-
messages:
|
|
86
|
-
temperature: options.temperature ?? 0.3,
|
|
212
|
+
messages: toAnthropicMessages(options.messages),
|
|
87
213
|
max_tokens: options.maxTokens ?? 4096,
|
|
88
214
|
};
|
|
215
|
+
if (acceptsSamplingParams(config.model)) {
|
|
216
|
+
body.temperature = options.temperature ?? 0.3;
|
|
217
|
+
}
|
|
89
218
|
if (systemMsg) {
|
|
90
219
|
body.system = systemMsg.content;
|
|
91
220
|
}
|
|
221
|
+
if (options.tools?.length) {
|
|
222
|
+
body.tools = options.tools.map(t => ({
|
|
223
|
+
name: t.name,
|
|
224
|
+
description: t.description,
|
|
225
|
+
input_schema: t.inputSchema,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
92
228
|
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
93
229
|
method: 'POST',
|
|
94
230
|
headers: {
|
|
@@ -103,12 +239,18 @@ function createAnthropicProvider(config) {
|
|
|
103
239
|
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
|
104
240
|
}
|
|
105
241
|
const data = await response.json();
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
.
|
|
242
|
+
const blocks = Array.isArray(data.content) ? data.content : [];
|
|
243
|
+
const content = blocks
|
|
244
|
+
.filter(b => b.type === 'text')
|
|
245
|
+
.map(b => b.text)
|
|
109
246
|
.join('');
|
|
247
|
+
const toolCalls = blocks
|
|
248
|
+
.filter(b => b.type === 'tool_use')
|
|
249
|
+
.map(b => ({ id: b.id, name: b.name, input: (b.input ?? {}) }));
|
|
110
250
|
return {
|
|
111
251
|
content,
|
|
252
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
253
|
+
stopReason: anthropicStopReason(data.stop_reason),
|
|
112
254
|
usage: data.usage ? {
|
|
113
255
|
promptTokens: data.usage.input_tokens,
|
|
114
256
|
completionTokens: data.usage.output_tokens,
|
|
@@ -118,4 +260,5 @@ function createAnthropicProvider(config) {
|
|
|
118
260
|
},
|
|
119
261
|
};
|
|
120
262
|
}
|
|
263
|
+
export { DEFAULT_MODELS };
|
|
121
264
|
//# sourceMappingURL=llm-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-provider.js","sourceRoot":"","sources":["../../src/llm/llm-provider.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;
|
|
1
|
+
{"version":3,"file":"llm-provider.js","sourceRoot":"","sources":["../../src/llm/llm-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Which provider a set of settings selects. */
|
|
2
|
+
export type LLMProviderName = 'anthropic' | 'openai';
|
|
3
|
+
/** Non-secret settings, safe to keep in the project. */
|
|
4
|
+
export interface LlmProjectSettings {
|
|
5
|
+
provider?: LLMProviderName;
|
|
6
|
+
model?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Per-provider secrets, stored outside the project. */
|
|
10
|
+
export interface LlmCredentials {
|
|
11
|
+
anthropic?: {
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
};
|
|
14
|
+
openai?: {
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Where a resolved value came from — surfaced in the UI so the key's origin is never a mystery. */
|
|
19
|
+
export type LlmSettingsOrigin = 'env' | 'env-file' | 'project-settings' | 'user-credentials' | 'default';
|
|
20
|
+
/** Default model per provider. Both are the current flagship at time of writing. */
|
|
21
|
+
export declare const DEFAULT_MODELS: Record<LLMProviderName, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a `.env` file body into key/value pairs.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately small: `KEY=value` per line, `#` comments, optional `export`
|
|
26
|
+
* prefix, and single- or double-quoted values (escapes are only expanded
|
|
27
|
+
* inside double quotes, matching the common dotenv behaviour). Anything more
|
|
28
|
+
* elaborate belongs in a real shell, not a settings file.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseEnvFile(contents: string): Record<string, string>;
|
|
31
|
+
/** Read `<projectRoot>/.env`, or an empty map when there is none. */
|
|
32
|
+
export declare function loadEnvFile(projectRoot: string): Record<string, string>;
|
|
33
|
+
export declare function projectSettingsPath(projectRoot: string): string;
|
|
34
|
+
export declare function loadLlmProjectSettings(projectRoot: string): LlmProjectSettings;
|
|
35
|
+
export declare function saveLlmProjectSettings(projectRoot: string, settings: LlmProjectSettings): void;
|
|
36
|
+
/**
|
|
37
|
+
* Where user credentials live.
|
|
38
|
+
*
|
|
39
|
+
* `MEMO_CREDENTIALS_PATH` overrides the default so CI, containers, and tests
|
|
40
|
+
* can point somewhere other than the real home directory — without it, a key a
|
|
41
|
+
* developer saved through the workbench would leak into their test runs.
|
|
42
|
+
*/
|
|
43
|
+
export declare function credentialsPath(): string;
|
|
44
|
+
export declare function loadLlmCredentials(): LlmCredentials;
|
|
45
|
+
/**
|
|
46
|
+
* Store an API key for one provider, merging with any key already held for the
|
|
47
|
+
* other. Written 0600 — this file is the one place MEMO persists a secret.
|
|
48
|
+
*/
|
|
49
|
+
export declare function saveLlmCredential(provider: LLMProviderName, apiKey: string): void;
|
|
50
|
+
/** Forget the stored key for one provider. */
|
|
51
|
+
export declare function clearLlmCredential(provider: LLMProviderName): void;
|
|
52
|
+
/** A resolved setting plus where it was found. */
|
|
53
|
+
export interface ResolvedLlmSettings {
|
|
54
|
+
provider?: LLMProviderName;
|
|
55
|
+
apiKey?: string;
|
|
56
|
+
model: string;
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
/** Where the API key came from. `undefined` when there is no key. */
|
|
59
|
+
keyOrigin?: LlmSettingsOrigin;
|
|
60
|
+
/** True when a key is available from any source. */
|
|
61
|
+
configured: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve provider settings across every layer.
|
|
65
|
+
*
|
|
66
|
+
* The chosen provider is whichever the project explicitly selects, or — absent
|
|
67
|
+
* that — whichever has a usable key, Anthropic first.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveLlmSettings(projectRoot?: string): ResolvedLlmSettings;
|
|
70
|
+
/**
|
|
71
|
+
* Settings safe to send to a browser client: everything except the key itself.
|
|
72
|
+
* The workbench needs to know a key exists and where it came from — never its value.
|
|
73
|
+
*/
|
|
74
|
+
export interface LlmSettingsStatus {
|
|
75
|
+
configured: boolean;
|
|
76
|
+
provider?: LLMProviderName;
|
|
77
|
+
model: string;
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
keyOrigin?: LlmSettingsOrigin;
|
|
80
|
+
/** True when the key came from a file MEMO can overwrite. */
|
|
81
|
+
keyEditable: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function llmSettingsStatus(projectRoot?: string): LlmSettingsStatus;
|
|
84
|
+
//# sourceMappingURL=llm-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-settings.d.ts","sourceRoot":"","sources":["../../src/llm/llm-settings.ts"],"names":[],"mappings":"AAmBA,gDAAgD;AAChD,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,QAAQ,CAAC;AAErD,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC3B,SAAS,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC;AAED,oGAAoG;AACpG,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,UAAU,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,SAAS,CAAC;AAEzG,oFAAoF;AACpF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAG1D,CAAC;AAOF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0BrE;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQvE;AAID,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAe9E;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAQ9F;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED,wBAAgB,kBAAkB,IAAI,cAAc,CASnD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAWjF;AAED,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAMlE;AAID,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;CACvB;AAoCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAgC5E;AAaD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAYzE"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ─── LLM Settings Resolution ─────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Resolves LLM provider settings from four sources, highest precedence first:
|
|
4
|
+
//
|
|
5
|
+
// 1. The real process environment (ANTHROPIC_API_KEY, …)
|
|
6
|
+
// 2. A project `.env` file (<projectRoot>/.env)
|
|
7
|
+
// 3. Project settings (<projectRoot>/.memo/llm.json)
|
|
8
|
+
// 4. User credentials (~/.memo/credentials.json)
|
|
9
|
+
//
|
|
10
|
+
// Only the last source is written by MEMO itself, and it lives outside the
|
|
11
|
+
// project so a key entered in the workbench can never be committed. Project
|
|
12
|
+
// settings hold provider/model/baseUrl and are safe to version — the loader
|
|
13
|
+
// ignores an `apiKey` there rather than encouraging a key into the repo.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
16
|
+
import { resolve, dirname } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
/** Default model per provider. Both are the current flagship at time of writing. */
|
|
19
|
+
export const DEFAULT_MODELS = {
|
|
20
|
+
anthropic: 'claude-opus-5',
|
|
21
|
+
openai: 'gpt-4o',
|
|
22
|
+
};
|
|
23
|
+
const PROJECT_SETTINGS_RELATIVE = ['.memo', 'llm.json'];
|
|
24
|
+
const CREDENTIALS_RELATIVE = ['.memo', 'credentials.json'];
|
|
25
|
+
// ─── .env parsing ────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `.env` file body into key/value pairs.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately small: `KEY=value` per line, `#` comments, optional `export`
|
|
30
|
+
* prefix, and single- or double-quoted values (escapes are only expanded
|
|
31
|
+
* inside double quotes, matching the common dotenv behaviour). Anything more
|
|
32
|
+
* elaborate belongs in a real shell, not a settings file.
|
|
33
|
+
*/
|
|
34
|
+
export function parseEnvFile(contents) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
37
|
+
const line = rawLine.trim();
|
|
38
|
+
if (!line || line.startsWith('#'))
|
|
39
|
+
continue;
|
|
40
|
+
const withoutExport = line.startsWith('export ') ? line.slice('export '.length).trim() : line;
|
|
41
|
+
const eq = withoutExport.indexOf('=');
|
|
42
|
+
if (eq <= 0)
|
|
43
|
+
continue;
|
|
44
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
45
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
46
|
+
continue;
|
|
47
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
48
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
49
|
+
value = value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"');
|
|
50
|
+
}
|
|
51
|
+
else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
52
|
+
value = value.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Unquoted values run to an unescaped `#`, so `KEY=value # note` works.
|
|
56
|
+
const hash = value.indexOf(' #');
|
|
57
|
+
if (hash >= 0)
|
|
58
|
+
value = value.slice(0, hash).trimEnd();
|
|
59
|
+
}
|
|
60
|
+
out[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Read `<projectRoot>/.env`, or an empty map when there is none. */
|
|
65
|
+
export function loadEnvFile(projectRoot) {
|
|
66
|
+
const path = resolve(projectRoot, '.env');
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return {};
|
|
69
|
+
try {
|
|
70
|
+
return parseEnvFile(readFileSync(path, 'utf8'));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ─── Project settings (no secrets) ───────────────────────────────────────────
|
|
77
|
+
export function projectSettingsPath(projectRoot) {
|
|
78
|
+
return resolve(projectRoot, ...PROJECT_SETTINGS_RELATIVE);
|
|
79
|
+
}
|
|
80
|
+
export function loadLlmProjectSettings(projectRoot) {
|
|
81
|
+
const path = projectSettingsPath(projectRoot);
|
|
82
|
+
if (!existsSync(path))
|
|
83
|
+
return {};
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
86
|
+
// A key here would be one `git add` away from being published. Drop it.
|
|
87
|
+
const { provider, model, baseUrl } = parsed;
|
|
88
|
+
return {
|
|
89
|
+
provider: provider === 'anthropic' || provider === 'openai' ? provider : undefined,
|
|
90
|
+
model: typeof model === 'string' && model ? model : undefined,
|
|
91
|
+
baseUrl: typeof baseUrl === 'string' && baseUrl ? baseUrl : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function saveLlmProjectSettings(projectRoot, settings) {
|
|
99
|
+
const path = projectSettingsPath(projectRoot);
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
const clean = {};
|
|
102
|
+
if (settings.provider)
|
|
103
|
+
clean.provider = settings.provider;
|
|
104
|
+
if (settings.model)
|
|
105
|
+
clean.model = settings.model;
|
|
106
|
+
if (settings.baseUrl)
|
|
107
|
+
clean.baseUrl = settings.baseUrl;
|
|
108
|
+
writeFileSync(path, `${JSON.stringify(clean, null, 2)}\n`, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
// ─── User credentials (secrets, outside the project) ─────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Where user credentials live.
|
|
113
|
+
*
|
|
114
|
+
* `MEMO_CREDENTIALS_PATH` overrides the default so CI, containers, and tests
|
|
115
|
+
* can point somewhere other than the real home directory — without it, a key a
|
|
116
|
+
* developer saved through the workbench would leak into their test runs.
|
|
117
|
+
*/
|
|
118
|
+
export function credentialsPath() {
|
|
119
|
+
const override = process.env.MEMO_CREDENTIALS_PATH;
|
|
120
|
+
if (override)
|
|
121
|
+
return resolve(override);
|
|
122
|
+
return resolve(homedir(), ...CREDENTIALS_RELATIVE);
|
|
123
|
+
}
|
|
124
|
+
export function loadLlmCredentials() {
|
|
125
|
+
const path = credentialsPath();
|
|
126
|
+
if (!existsSync(path))
|
|
127
|
+
return {};
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
130
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Store an API key for one provider, merging with any key already held for the
|
|
138
|
+
* other. Written 0600 — this file is the one place MEMO persists a secret.
|
|
139
|
+
*/
|
|
140
|
+
export function saveLlmCredential(provider, apiKey) {
|
|
141
|
+
const path = credentialsPath();
|
|
142
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
143
|
+
const existing = loadLlmCredentials();
|
|
144
|
+
const next = { ...existing, [provider]: { apiKey } };
|
|
145
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
146
|
+
try {
|
|
147
|
+
chmodSync(path, 0o600);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Best effort — a filesystem without POSIX modes is not a reason to fail the save.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Forget the stored key for one provider. */
|
|
154
|
+
export function clearLlmCredential(provider) {
|
|
155
|
+
const path = credentialsPath();
|
|
156
|
+
if (!existsSync(path))
|
|
157
|
+
return;
|
|
158
|
+
const existing = loadLlmCredentials();
|
|
159
|
+
delete existing[provider];
|
|
160
|
+
writeFileSync(path, `${JSON.stringify(existing, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
161
|
+
}
|
|
162
|
+
function readLayers(projectRoot) {
|
|
163
|
+
return {
|
|
164
|
+
env: process.env,
|
|
165
|
+
envFile: projectRoot ? loadEnvFile(projectRoot) : {},
|
|
166
|
+
project: projectRoot ? loadLlmProjectSettings(projectRoot) : {},
|
|
167
|
+
credentials: loadLlmCredentials(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** First non-empty value for `name` across the two environment layers. */
|
|
171
|
+
function fromEnvLayers(layers, name) {
|
|
172
|
+
const direct = layers.env[name];
|
|
173
|
+
if (direct)
|
|
174
|
+
return { value: direct, origin: 'env' };
|
|
175
|
+
const fromFile = layers.envFile[name];
|
|
176
|
+
if (fromFile)
|
|
177
|
+
return { value: fromFile, origin: 'env-file' };
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function resolveKey(layers, provider) {
|
|
181
|
+
const envName = provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
|
|
182
|
+
const fromEnv = fromEnvLayers(layers, envName);
|
|
183
|
+
if (fromEnv)
|
|
184
|
+
return fromEnv;
|
|
185
|
+
const stored = layers.credentials[provider]?.apiKey;
|
|
186
|
+
if (stored)
|
|
187
|
+
return { value: stored, origin: 'user-credentials' };
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve provider settings across every layer.
|
|
192
|
+
*
|
|
193
|
+
* The chosen provider is whichever the project explicitly selects, or — absent
|
|
194
|
+
* that — whichever has a usable key, Anthropic first.
|
|
195
|
+
*/
|
|
196
|
+
export function resolveLlmSettings(projectRoot) {
|
|
197
|
+
const layers = readLayers(projectRoot);
|
|
198
|
+
const preferred = layers.project.provider;
|
|
199
|
+
const order = preferred
|
|
200
|
+
? [preferred, preferred === 'anthropic' ? 'openai' : 'anthropic']
|
|
201
|
+
: ['anthropic', 'openai'];
|
|
202
|
+
for (const provider of order) {
|
|
203
|
+
const key = resolveKey(layers, provider);
|
|
204
|
+
if (!key)
|
|
205
|
+
continue;
|
|
206
|
+
return {
|
|
207
|
+
provider,
|
|
208
|
+
apiKey: key.value,
|
|
209
|
+
model: resolveModel(layers, provider),
|
|
210
|
+
baseUrl: resolveBaseUrl(layers, provider),
|
|
211
|
+
keyOrigin: key.origin,
|
|
212
|
+
configured: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// No key anywhere. Still report the provider/model the project asks for so
|
|
216
|
+
// the settings screen can show what would be used once a key is supplied.
|
|
217
|
+
const provider = preferred;
|
|
218
|
+
return {
|
|
219
|
+
provider,
|
|
220
|
+
apiKey: undefined,
|
|
221
|
+
model: provider ? resolveModel(layers, provider) : DEFAULT_MODELS.anthropic,
|
|
222
|
+
baseUrl: provider ? resolveBaseUrl(layers, provider) : undefined,
|
|
223
|
+
keyOrigin: undefined,
|
|
224
|
+
configured: false,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function resolveModel(layers, provider) {
|
|
228
|
+
return fromEnvLayers(layers, 'MEMO_LLM_MODEL')?.value
|
|
229
|
+
?? layers.project.model
|
|
230
|
+
?? DEFAULT_MODELS[provider];
|
|
231
|
+
}
|
|
232
|
+
function resolveBaseUrl(layers, provider) {
|
|
233
|
+
const envName = provider === 'anthropic' ? 'ANTHROPIC_BASE_URL' : 'OPENAI_BASE_URL';
|
|
234
|
+
return fromEnvLayers(layers, envName)?.value ?? layers.project.baseUrl;
|
|
235
|
+
}
|
|
236
|
+
export function llmSettingsStatus(projectRoot) {
|
|
237
|
+
const resolved = resolveLlmSettings(projectRoot);
|
|
238
|
+
return {
|
|
239
|
+
configured: resolved.configured,
|
|
240
|
+
provider: resolved.provider,
|
|
241
|
+
model: resolved.model,
|
|
242
|
+
baseUrl: resolved.baseUrl,
|
|
243
|
+
keyOrigin: resolved.keyOrigin,
|
|
244
|
+
// An env-supplied key wins over the credentials file, so offering to
|
|
245
|
+
// overwrite it in the UI would be a lie — saving would change nothing.
|
|
246
|
+
keyEditable: !resolved.configured || resolved.keyOrigin === 'user-credentials',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=llm-settings.js.map
|