@memoarchitect/tools 0.6.3 → 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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/tools/lib/bin/memo.js +38 -2
  4. package/packages/tools/lib/bin/memo.js.map +1 -1
  5. package/packages/tools/lib/browser/index.d.ts +3 -0
  6. package/packages/tools/lib/browser/index.d.ts.map +1 -1
  7. package/packages/tools/lib/commands/init.d.ts +2 -0
  8. package/packages/tools/lib/commands/init.d.ts.map +1 -1
  9. package/packages/tools/lib/commands/init.js +32 -16
  10. package/packages/tools/lib/commands/init.js.map +1 -1
  11. package/packages/tools/lib/commands/mcp.d.ts +15 -0
  12. package/packages/tools/lib/commands/mcp.d.ts.map +1 -0
  13. package/packages/tools/lib/commands/mcp.js +128 -0
  14. package/packages/tools/lib/commands/mcp.js.map +1 -0
  15. package/packages/tools/lib/index.d.ts.map +1 -1
  16. package/packages/tools/lib/index.js +4 -0
  17. package/packages/tools/lib/index.js.map +1 -1
  18. package/packages/tools/lib/llm/chat-engine.d.ts +97 -0
  19. package/packages/tools/lib/llm/chat-engine.d.ts.map +1 -0
  20. package/packages/tools/lib/llm/chat-engine.js +463 -0
  21. package/packages/tools/lib/llm/chat-engine.js.map +1 -0
  22. package/packages/tools/lib/llm/index.d.ts +2 -0
  23. package/packages/tools/lib/llm/index.d.ts.map +1 -1
  24. package/packages/tools/lib/llm/index.js +2 -0
  25. package/packages/tools/lib/llm/index.js.map +1 -1
  26. package/packages/tools/lib/llm/llm-provider.d.ts +40 -4
  27. package/packages/tools/lib/llm/llm-provider.d.ts.map +1 -1
  28. package/packages/tools/lib/llm/llm-provider.js +180 -37
  29. package/packages/tools/lib/llm/llm-provider.js.map +1 -1
  30. package/packages/tools/lib/llm/llm-settings.d.ts +84 -0
  31. package/packages/tools/lib/llm/llm-settings.d.ts.map +1 -0
  32. package/packages/tools/lib/llm/llm-settings.js +249 -0
  33. package/packages/tools/lib/llm/llm-settings.js.map +1 -0
  34. package/packages/tools/lib/mcp/index.d.ts +4 -0
  35. package/packages/tools/lib/mcp/index.d.ts.map +1 -0
  36. package/packages/tools/lib/mcp/index.js +9 -0
  37. package/packages/tools/lib/mcp/index.js.map +1 -0
  38. package/packages/tools/lib/mcp/model-loader.d.ts +11 -0
  39. package/packages/tools/lib/mcp/model-loader.d.ts.map +1 -0
  40. package/packages/tools/lib/mcp/model-loader.js +87 -0
  41. package/packages/tools/lib/mcp/model-loader.js.map +1 -0
  42. package/packages/tools/lib/mcp/server.d.ts +8 -0
  43. package/packages/tools/lib/mcp/server.d.ts.map +1 -0
  44. package/packages/tools/lib/mcp/server.js +132 -0
  45. package/packages/tools/lib/mcp/server.js.map +1 -0
  46. package/packages/tools/lib/mcp/tools.d.ts +18 -0
  47. package/packages/tools/lib/mcp/tools.d.ts.map +1 -0
  48. package/packages/tools/lib/mcp/tools.js +220 -0
  49. package/packages/tools/lib/mcp/tools.js.map +1 -0
  50. package/packages/tools/lib/model/builder.js +1 -1
  51. package/packages/tools/lib/model/builder.js.map +1 -1
  52. package/packages/tools/lib/model/view-deriver.d.ts +3 -2
  53. package/packages/tools/lib/model/view-deriver.d.ts.map +1 -1
  54. package/packages/tools/lib/model/view-deriver.js +33 -10
  55. package/packages/tools/lib/model/view-deriver.js.map +1 -1
  56. package/packages/tools/lib/protocol/messages.d.ts +88 -2
  57. package/packages/tools/lib/protocol/messages.d.ts.map +1 -1
  58. package/packages/tools/lib/server/dev-server.d.ts.map +1 -1
  59. package/packages/tools/lib/server/dev-server.js +194 -10
  60. 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. Configured via env vars.
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
- /** Resolve LLM config from environment variables */
7
- export function resolveLLMConfig() {
8
- // Anthropic takes priority if both are set
9
- if (process.env.ANTHROPIC_API_KEY) {
10
- return {
11
- provider: 'anthropic',
12
- apiKey: process.env.ANTHROPIC_API_KEY,
13
- model: process.env.MEMO_LLM_MODEL || 'claude-sonnet-4-20250514',
14
- baseUrl: process.env.ANTHROPIC_BASE_URL,
15
- };
16
- }
17
- if (process.env.OPENAI_API_KEY) {
18
- return {
19
- provider: 'openai',
20
- apiKey: process.env.OPENAI_API_KEY,
21
- model: process.env.MEMO_LLM_MODEL || 'gpt-4o',
22
- baseUrl: process.env.OPENAI_BASE_URL,
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: data.choices[0].message.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: chatMessages,
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 content = data.content
107
- .filter((b) => b.type === 'text')
108
- .map((b) => b.text)
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":"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"}
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