amalgm 0.1.51 → 0.1.52
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/lib/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* Event triggers storage — ~/.amalgm/event-triggers.json
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { EVENT_TRIGGERS_FILE, STORAGE_DIR } = require('../config');
|
|
6
7
|
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
7
8
|
const { openLocalDb } = require('../state/db');
|
|
8
|
-
const {
|
|
9
|
-
chatInputToLegacyFields,
|
|
10
|
-
getChatInputText,
|
|
11
|
-
normalizeChatInput,
|
|
12
|
-
} = require('../../../lib/chatInput');
|
|
13
|
-
const { DEFAULT_SELECTED_MODELS, getSelectedModel } = require('../lib/prefs');
|
|
14
|
-
const credentialAdapter = require('../../credential-adapter');
|
|
15
9
|
const { appendStateEvent, insertStateEvent, publishStateEvent } = require('../state/events');
|
|
10
|
+
const { compileWorkflowText } = require('../workflows/compiler');
|
|
11
|
+
const { syncWorkflowTriggerLinks, upsertWorkflowFromTrigger } = require('../workflows/store');
|
|
12
|
+
const { internalWorkflowSeeds } = require('./internal-workflows');
|
|
13
|
+
const { getWebhookUrl } = require('./webhook-url');
|
|
16
14
|
|
|
17
15
|
const DEFAULT_RUN_LIMIT = 100;
|
|
18
16
|
|
|
@@ -29,66 +27,111 @@ function parseJson(value, fallback = null) {
|
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
projectPath: trigger.projectPath,
|
|
64
|
-
});
|
|
30
|
+
function normalizeStringList(values) {
|
|
31
|
+
if (!Array.isArray(values)) return [];
|
|
32
|
+
return [...new Set(values
|
|
33
|
+
.filter((value) => typeof value === 'string')
|
|
34
|
+
.map((value) => value.trim())
|
|
35
|
+
.filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeWorkflowTrigger(trigger) {
|
|
39
|
+
const workflowText = String(trigger.workflowText || trigger.workflow || '').trim();
|
|
40
|
+
let workflowIr = trigger.workflowIr && trigger.workflowIr.version === 1 ? trigger.workflowIr : null;
|
|
41
|
+
let compilerErrors = null;
|
|
42
|
+
|
|
43
|
+
if (workflowText) {
|
|
44
|
+
try {
|
|
45
|
+
workflowIr = compileWorkflowText(workflowText);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
workflowIr = null;
|
|
48
|
+
compilerErrors = [error.message || String(error)];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const irTrigger = workflowIr?.trigger || {};
|
|
53
|
+
const allowlist = {
|
|
54
|
+
...(workflowIr?.allowlist || {}),
|
|
55
|
+
...(trigger.allowlist && typeof trigger.allowlist === 'object' ? trigger.allowlist : {}),
|
|
56
|
+
};
|
|
57
|
+
const limits = {
|
|
58
|
+
...(workflowIr?.limits || {}),
|
|
59
|
+
...(trigger.limits && typeof trigger.limits === 'object' ? trigger.limits : {}),
|
|
60
|
+
};
|
|
65
61
|
|
|
66
62
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
id: trigger.id,
|
|
64
|
+
name: trigger.name || 'Untitled workflow',
|
|
65
|
+
description: trigger.description || '',
|
|
66
|
+
source: irTrigger.source || trigger.source || '*',
|
|
67
|
+
event: irTrigger.event || trigger.event || '*',
|
|
68
|
+
enabled: trigger.enabled !== false,
|
|
69
|
+
webhookUrl: trigger.webhookUrl || getWebhookUrl(),
|
|
70
|
+
secret: trigger.secret || crypto.randomUUID().replace(/-/g, ''),
|
|
71
|
+
projectPath: trigger.projectPath || null,
|
|
72
|
+
workflowIds: normalizeStringList(
|
|
73
|
+
trigger.workflowIds || (trigger.workflowId ? [trigger.workflowId] : []),
|
|
74
|
+
),
|
|
75
|
+
workflowText,
|
|
76
|
+
workflowIr,
|
|
77
|
+
compilerErrors,
|
|
78
|
+
allowlist,
|
|
79
|
+
limits,
|
|
80
|
+
internal: trigger.internal === true,
|
|
81
|
+
createdAt: trigger.createdAt || nowIso(),
|
|
82
|
+
updatedAt: trigger.updatedAt || null,
|
|
83
|
+
lastFiredAt: trigger.lastFiredAt || null,
|
|
74
84
|
};
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
function migrateTriggersData(data) {
|
|
78
88
|
let changed = false;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const rawTriggers = Array.isArray(data.triggers) ? data.triggers : [];
|
|
90
|
+
const triggers = rawTriggers
|
|
91
|
+
.filter((trigger) => trigger && (
|
|
92
|
+
trigger.workflowText
|
|
93
|
+
|| trigger.workflow
|
|
94
|
+
|| trigger.workflowIr
|
|
95
|
+
|| trigger.workflowId
|
|
96
|
+
|| (Array.isArray(trigger.workflowIds) && trigger.workflowIds.length > 0)
|
|
97
|
+
))
|
|
98
|
+
.map((trigger) => {
|
|
99
|
+
const embeddedWorkflow = upsertWorkflowFromTrigger(trigger);
|
|
100
|
+
const normalizedTrigger = normalizeWorkflowTrigger({
|
|
101
|
+
...trigger,
|
|
102
|
+
workflowIds: normalizeStringList([
|
|
103
|
+
...(Array.isArray(trigger.workflowIds) ? trigger.workflowIds : []),
|
|
104
|
+
...(trigger.workflowId ? [trigger.workflowId] : []),
|
|
105
|
+
...(embeddedWorkflow?.id ? [embeddedWorkflow.id] : []),
|
|
106
|
+
]),
|
|
107
|
+
});
|
|
108
|
+
if (JSON.stringify(normalizedTrigger) !== JSON.stringify(trigger)) {
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
return normalizedTrigger;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (triggers.length !== rawTriggers.length) changed = true;
|
|
115
|
+
|
|
116
|
+
const existingIds = new Set(triggers.map((trigger) => trigger.id));
|
|
117
|
+
for (const seed of internalWorkflowSeeds()) {
|
|
118
|
+
if (existingIds.has(seed.id)) continue;
|
|
119
|
+
triggers.push(normalizeWorkflowTrigger({
|
|
120
|
+
...seed,
|
|
121
|
+
webhookUrl: seed.webhookUrl || null,
|
|
122
|
+
secret: seed.secret || null,
|
|
123
|
+
createdAt: nowIso(),
|
|
124
|
+
lastFiredAt: null,
|
|
125
|
+
}));
|
|
126
|
+
changed = true;
|
|
127
|
+
}
|
|
88
128
|
|
|
89
129
|
return {
|
|
90
130
|
changed,
|
|
91
|
-
data: {
|
|
131
|
+
data: {
|
|
132
|
+
version: 2,
|
|
133
|
+
triggers,
|
|
134
|
+
},
|
|
92
135
|
};
|
|
93
136
|
}
|
|
94
137
|
|
|
@@ -124,6 +167,7 @@ function publishEventTriggersChange(data, source = 'event-triggers') {
|
|
|
124
167
|
|
|
125
168
|
function saveEventTriggers(data, options = {}) {
|
|
126
169
|
writeJsonAtomic(EVENT_TRIGGERS_FILE, data);
|
|
170
|
+
syncWorkflowTriggerLinks(data.triggers);
|
|
127
171
|
publishEventTriggersChange(data, options.source || 'event-triggers:save');
|
|
128
172
|
}
|
|
129
173
|
|
|
@@ -240,6 +284,8 @@ function mergeEventRunEntry(existing, triggerId, entry) {
|
|
|
240
284
|
runId: entry.runId || entry.id || existing?.id || entry.sessionId || null,
|
|
241
285
|
triggerId,
|
|
242
286
|
triggerName: entry.triggerName || existing?.triggerName || null,
|
|
287
|
+
workflowId: entry.workflowId || existing?.workflowId || null,
|
|
288
|
+
workflowName: entry.workflowName || existing?.workflowName || null,
|
|
243
289
|
status: entry.status || existing?.status || 'running',
|
|
244
290
|
startedAt,
|
|
245
291
|
finishedAt,
|
|
@@ -248,6 +294,8 @@ function mergeEventRunEntry(existing, triggerId, entry) {
|
|
|
248
294
|
projectPath: entry.projectPath || existing?.projectPath || null,
|
|
249
295
|
error: entry.error || existing?.error || null,
|
|
250
296
|
durationMs: entry.durationMs ?? existing?.durationMs ?? null,
|
|
297
|
+
workflow: entry.workflow ?? existing?.workflow ?? null,
|
|
298
|
+
output: entry.output ?? existing?.output ?? null,
|
|
251
299
|
outputLength: entry.outputLength ?? existing?.outputLength ?? null,
|
|
252
300
|
prompt: entry.prompt || existing?.prompt || null,
|
|
253
301
|
events,
|
|
@@ -308,6 +356,7 @@ module.exports = {
|
|
|
308
356
|
ensureTriggersDirs,
|
|
309
357
|
listEventRuns,
|
|
310
358
|
loadEventTriggers,
|
|
359
|
+
normalizeWorkflowTrigger,
|
|
311
360
|
recordEventRun,
|
|
312
361
|
saveEventTriggers,
|
|
313
362
|
};
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Modules:
|
|
13
13
|
* config.js env + paths
|
|
14
|
-
* deps.js
|
|
14
|
+
* deps.js optional runtime dependency loaders
|
|
15
15
|
* lib/ generic utilities (storage, chat-runner,
|
|
16
16
|
* supabase, prefs, email-md, tool-result)
|
|
17
|
-
*
|
|
18
|
-
* events/
|
|
17
|
+
* automations/ triggers + workflows (MCP tools + scheduler)
|
|
18
|
+
* events/ webhook ingress runtime for event triggers
|
|
19
19
|
* agents/ agents CRUD + talk_to_agent
|
|
20
|
-
* browser/ visible Electron browser +
|
|
20
|
+
* browser/ visible Electron browser tools + agent-browser fallback
|
|
21
21
|
* notify/ notify_user (email via proxy)
|
|
22
22
|
* server/ MCP JSON-RPC dispatch + HTTP route table
|
|
23
23
|
*/
|
|
@@ -75,23 +75,21 @@ async function ensureProxyToken() {
|
|
|
75
75
|
async function boot() {
|
|
76
76
|
await ensureProxyToken();
|
|
77
77
|
|
|
78
|
-
const {
|
|
79
|
-
const { ensureTriggersDirs } = require('./events/store');
|
|
78
|
+
const { ensureAutomationsStore } = require('./automations/store');
|
|
80
79
|
const { ensureAgentsDirs } = require('./agents/store');
|
|
81
|
-
const {
|
|
82
|
-
const {
|
|
83
|
-
const {
|
|
80
|
+
const { ensureAppsDirs } = require('./apps/store');
|
|
81
|
+
const { startAppRouteSyncLoop } = require('./apps/advertise');
|
|
82
|
+
const { startRegisteredApps } = require('./apps/supervisor');
|
|
84
83
|
const { listen } = require('./server/http');
|
|
85
84
|
|
|
86
85
|
// Make sure every storage dir/file exists before we accept traffic.
|
|
87
|
-
|
|
88
|
-
ensureTriggersDirs();
|
|
86
|
+
ensureAutomationsStore();
|
|
89
87
|
ensureAgentsDirs();
|
|
90
|
-
|
|
88
|
+
ensureAppsDirs();
|
|
91
89
|
|
|
92
90
|
await listen();
|
|
93
|
-
await
|
|
94
|
-
|
|
91
|
+
await startRegisteredApps();
|
|
92
|
+
startAppRouteSyncLoop();
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
boot().catch((err) => {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User model preferences — mirrors the Zustand harnessPreferences store.
|
|
3
3
|
*
|
|
4
|
-
* Hydrates on boot (non-blocking) from
|
|
4
|
+
* Hydrates on boot (non-blocking) from local SQLite. Used to pick the right model
|
|
5
5
|
* for task/event/agent runs without requiring the caller to specify it.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
8
|
+
const { openLocalDb } = require('../state/db');
|
|
9
|
+
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
10
10
|
|
|
11
11
|
const DEFAULT_SELECTED_MODELS = {
|
|
12
12
|
claude_code: 'anthropic/claude-opus-4.7',
|
|
13
|
-
codex: 'openai/gpt-5.
|
|
14
|
-
opencode: 'opencode-
|
|
13
|
+
codex: 'openai/gpt-5.5',
|
|
14
|
+
opencode: 'opencode-deepseek-deepseek-v4-pro',
|
|
15
15
|
pi: 'pi-anthropic-claude-sonnet-4.6',
|
|
16
16
|
amp: 'amp-default',
|
|
17
17
|
cursor: 'cursor/composer-2-fast',
|
|
@@ -124,10 +124,16 @@ const CURSOR_MODEL_ALIASES = {
|
|
|
124
124
|
'cursor-moonshotai/kimi-k2.5': 'moonshotai/kimi-k2.5',
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
const PREFERENCES_META_KEY = 'user_preferences';
|
|
128
|
+
const PREFERENCES_RESOURCE = 'user_preferences';
|
|
129
|
+
const PREFERENCES_RESOURCE_ID = 'current';
|
|
130
|
+
|
|
127
131
|
const _selectedModels = { ...DEFAULT_SELECTED_MODELS };
|
|
128
|
-
const _authMethods = {};
|
|
129
|
-
let
|
|
130
|
-
let
|
|
132
|
+
const _authMethods = { ...DEFAULT_AUTH_METHODS };
|
|
133
|
+
let _lastUsedHarness = 'claude_code';
|
|
134
|
+
let _lastUsedModel = DEFAULT_SELECTED_MODELS.claude_code;
|
|
135
|
+
let _lastUsedCwd = null;
|
|
136
|
+
let _modelSettings = {};
|
|
131
137
|
let _prefsHydrated = false;
|
|
132
138
|
let _prefsHydratePromise = null;
|
|
133
139
|
|
|
@@ -294,26 +300,212 @@ function normalizeModelSelection(harnessId, modelId) {
|
|
|
294
300
|
return { modelId, reasoningEffort: null };
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
function isPlainObject(value) {
|
|
304
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function cleanString(value) {
|
|
308
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function coerceHarnessId(value, fallback = 'claude_code') {
|
|
312
|
+
return Object.prototype.hasOwnProperty.call(DEFAULT_SELECTED_MODELS, value)
|
|
313
|
+
? value
|
|
314
|
+
: fallback;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeModelSettings(value) {
|
|
318
|
+
if (!isPlainObject(value)) return {};
|
|
319
|
+
const normalized = {};
|
|
320
|
+
for (const [key, settings] of Object.entries(value)) {
|
|
321
|
+
if (!key || !isPlainObject(settings)) continue;
|
|
322
|
+
if (Object.keys(settings).length === 0) continue;
|
|
323
|
+
normalized[key] = settings;
|
|
324
|
+
}
|
|
325
|
+
return normalized;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeReasoningEffort(value) {
|
|
329
|
+
if (typeof value !== 'string') return null;
|
|
330
|
+
const normalized = value.trim().toLowerCase().replace(/_/g, '-');
|
|
331
|
+
if (normalized === 'low') return 'low';
|
|
332
|
+
if (normalized === 'medium') return 'medium';
|
|
333
|
+
if (normalized === 'high') return 'high';
|
|
334
|
+
if (normalized === 'xhigh' || normalized === 'extra-high' || normalized === 'extra high') {
|
|
335
|
+
return 'xhigh';
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function codexModelSettingsKey(modelId) {
|
|
341
|
+
return codexGatewayModelId(String(modelId || ''));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function reasoningEffortForSelection(harnessId, modelId, normalizedEffort) {
|
|
345
|
+
const explicitEffort = normalizeReasoningEffort(normalizedEffort);
|
|
346
|
+
if (explicitEffort) return explicitEffort;
|
|
347
|
+
if (harnessId !== 'codex') return null;
|
|
348
|
+
const settingsKey = codexModelSettingsKey(modelId);
|
|
349
|
+
const settings = _modelSettings[settingsKey] || _modelSettings[modelId] || {};
|
|
350
|
+
return normalizeReasoningEffort(settings.effort || settings.reasoningEffort || settings.reasoning) || 'xhigh';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizePreferences(input) {
|
|
354
|
+
const raw = isPlainObject(input) ? input : {};
|
|
355
|
+
const rawRecentPrefs = isPlainObject(raw.recent_prefs) ? raw.recent_prefs : {};
|
|
356
|
+
const recentPrefs = { ...DEFAULT_SELECTED_MODELS };
|
|
357
|
+
for (const [harnessId, modelId] of Object.entries(rawRecentPrefs)) {
|
|
358
|
+
if (!modelId) continue;
|
|
359
|
+
const harness = coerceHarnessId(harnessId, null);
|
|
360
|
+
if (!harness) continue;
|
|
361
|
+
recentPrefs[harness] = normalizeModelSelection(harness, modelId).modelId || modelId;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const rawUsage = isPlainObject(raw.usage) ? raw.usage : {};
|
|
365
|
+
const usage = { ...DEFAULT_AUTH_METHODS };
|
|
366
|
+
for (const harnessId of Object.keys(DEFAULT_AUTH_METHODS)) {
|
|
367
|
+
usage[harnessId] = coerceAuthMethodForHarness(harnessId, rawUsage[harnessId] || usage[harnessId]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const rawLastUsed = isPlainObject(raw.last_used) ? raw.last_used : {};
|
|
371
|
+
const lastUsedHarness = coerceHarnessId(rawLastUsed.harness, 'claude_code');
|
|
372
|
+
const rawLastModel =
|
|
373
|
+
rawLastUsed.model ||
|
|
374
|
+
recentPrefs[lastUsedHarness] ||
|
|
375
|
+
DEFAULT_SELECTED_MODELS[lastUsedHarness];
|
|
376
|
+
const lastUsedModel =
|
|
377
|
+
normalizeModelSelection(lastUsedHarness, rawLastModel).modelId ||
|
|
378
|
+
rawLastModel;
|
|
379
|
+
recentPrefs[lastUsedHarness] = lastUsedModel;
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
last_used: {
|
|
383
|
+
harness: lastUsedHarness,
|
|
384
|
+
model: lastUsedModel,
|
|
385
|
+
cwd: cleanString(rawLastUsed.cwd ?? raw.last_cwd ?? raw.selected_cwd),
|
|
386
|
+
},
|
|
387
|
+
usage,
|
|
388
|
+
recent_prefs: recentPrefs,
|
|
389
|
+
model_settings: normalizeModelSettings(raw.model_settings),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function applyPreferenceCache(preferences) {
|
|
394
|
+
const normalized = normalizePreferences(preferences);
|
|
395
|
+
_lastUsedHarness = normalized.last_used.harness;
|
|
396
|
+
_lastUsedModel = normalized.last_used.model;
|
|
397
|
+
_lastUsedCwd = normalized.last_used.cwd;
|
|
398
|
+
_modelSettings = normalized.model_settings;
|
|
399
|
+
|
|
400
|
+
for (const key of Object.keys(_selectedModels)) delete _selectedModels[key];
|
|
401
|
+
Object.assign(_selectedModels, normalized.recent_prefs);
|
|
402
|
+
|
|
403
|
+
for (const key of Object.keys(_authMethods)) delete _authMethods[key];
|
|
404
|
+
Object.assign(_authMethods, normalized.usage);
|
|
405
|
+
|
|
406
|
+
return normalized;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseStoredPreferences(value) {
|
|
410
|
+
if (typeof value !== 'string' || !value) return {};
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(value);
|
|
413
|
+
} catch {
|
|
414
|
+
return {};
|
|
309
415
|
}
|
|
310
|
-
|
|
311
|
-
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function readUserPreferences() {
|
|
419
|
+
const row = openLocalDb()
|
|
420
|
+
.prepare('SELECT value FROM local_meta WHERE key = ?')
|
|
421
|
+
.get(PREFERENCES_META_KEY);
|
|
422
|
+
_prefsHydrated = true;
|
|
423
|
+
return applyPreferenceCache(normalizePreferences(parseStoredPreferences(row?.value)));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function writeUserPreferences(preferences, options = {}) {
|
|
427
|
+
const normalized = normalizePreferences(preferences);
|
|
428
|
+
const db = openLocalDb();
|
|
429
|
+
const event = db.transaction(() => {
|
|
430
|
+
db.prepare(`
|
|
431
|
+
INSERT INTO local_meta (key, value, updated_at)
|
|
432
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
433
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
434
|
+
value = excluded.value,
|
|
435
|
+
updated_at = excluded.updated_at
|
|
436
|
+
`).run(PREFERENCES_META_KEY, JSON.stringify(normalized));
|
|
437
|
+
|
|
438
|
+
return insertStateEvent(db, {
|
|
439
|
+
resource: PREFERENCES_RESOURCE,
|
|
440
|
+
op: 'replace',
|
|
441
|
+
id: PREFERENCES_RESOURCE_ID,
|
|
442
|
+
value: normalized,
|
|
443
|
+
source: options.source || 'user-preferences',
|
|
444
|
+
});
|
|
445
|
+
})();
|
|
446
|
+
|
|
447
|
+
_prefsHydrated = true;
|
|
448
|
+
applyPreferenceCache(normalized);
|
|
449
|
+
publishStateEvent(event);
|
|
450
|
+
return normalized;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function mergePreferencesPatch(current, patch) {
|
|
454
|
+
const body = isPlainObject(patch) ? patch : {};
|
|
455
|
+
const bodyLastUsed = isPlainObject(body.last_used) ? body.last_used : {};
|
|
456
|
+
const lastUsedHarness = coerceHarnessId(
|
|
457
|
+
bodyLastUsed.harness,
|
|
458
|
+
current.last_used.harness,
|
|
459
|
+
);
|
|
460
|
+
const bodyRecentPrefs = isPlainObject(body.recent_prefs) ? body.recent_prefs : {};
|
|
461
|
+
const recentPrefs = {
|
|
462
|
+
...current.recent_prefs,
|
|
463
|
+
...bodyRecentPrefs,
|
|
464
|
+
};
|
|
465
|
+
const lastUsedModel =
|
|
466
|
+
bodyLastUsed.model ||
|
|
467
|
+
recentPrefs[lastUsedHarness] ||
|
|
468
|
+
current.last_used.model;
|
|
469
|
+
const cwd = cleanString(
|
|
470
|
+
bodyLastUsed.cwd ??
|
|
471
|
+
body.selectedCwd ??
|
|
472
|
+
body.cwd ??
|
|
473
|
+
current.last_used.cwd,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
return normalizePreferences({
|
|
477
|
+
last_used: {
|
|
478
|
+
harness: lastUsedHarness,
|
|
479
|
+
model: lastUsedModel,
|
|
480
|
+
cwd,
|
|
481
|
+
},
|
|
482
|
+
usage: isPlainObject(body.usage)
|
|
483
|
+
? { ...current.usage, ...body.usage }
|
|
484
|
+
: current.usage,
|
|
485
|
+
recent_prefs: recentPrefs,
|
|
486
|
+
model_settings: mergeModelSettingsPatch(current.model_settings, body.model_settings),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function mergeModelSettingsPatch(currentModelSettings, patchModelSettings) {
|
|
491
|
+
if (!isPlainObject(patchModelSettings)) return currentModelSettings;
|
|
492
|
+
const next = { ...currentModelSettings };
|
|
493
|
+
for (const [key, value] of Object.entries(patchModelSettings)) {
|
|
494
|
+
if (!key || !isPlainObject(value) || Object.keys(value).length === 0) {
|
|
495
|
+
delete next[key];
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
next[key] = value;
|
|
312
499
|
}
|
|
313
|
-
return
|
|
500
|
+
return next;
|
|
314
501
|
}
|
|
315
502
|
|
|
316
|
-
function
|
|
503
|
+
function updateUserPreferences(patch, options = {}) {
|
|
504
|
+
const current = readUserPreferences();
|
|
505
|
+
return writeUserPreferences(mergePreferencesPatch(current, patch), options);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function resolveCliModel(harnessId, modelId, reasoningEffort = null) {
|
|
317
509
|
const selection = normalizeModelSelection(harnessId, modelId);
|
|
318
510
|
const selectedModelId = selection.modelId || modelId;
|
|
319
511
|
if (harnessId === 'claude_code') {
|
|
@@ -321,7 +513,10 @@ function resolveCliModel(harnessId, modelId) {
|
|
|
321
513
|
selectedModelId.replace(/^anthropic\//, '').replace(/-(\d+)\.(\d+)(?=$|-)/, '-$1-$2');
|
|
322
514
|
}
|
|
323
515
|
if (harnessId === 'codex') {
|
|
324
|
-
return codexHarnessModelId(
|
|
516
|
+
return codexHarnessModelId(
|
|
517
|
+
selectedModelId,
|
|
518
|
+
reasoningEffortForSelection(harnessId, selectedModelId, reasoningEffort || selection.reasoningEffort),
|
|
519
|
+
);
|
|
325
520
|
}
|
|
326
521
|
if (harnessId === 'cursor') {
|
|
327
522
|
return cursorCanonicalModelIdToCliModel(selectedModelId);
|
|
@@ -347,50 +542,16 @@ function resolveCliModel(harnessId, modelId) {
|
|
|
347
542
|
|
|
348
543
|
async function hydrateModelPreferences() {
|
|
349
544
|
if (_prefsHydratePromise) return _prefsHydratePromise;
|
|
350
|
-
if (_prefsHydrated
|
|
545
|
+
if (_prefsHydrated) return;
|
|
351
546
|
_prefsHydratePromise = (async () => {
|
|
352
|
-
_prefsHydrated = true;
|
|
353
547
|
try {
|
|
354
|
-
const
|
|
355
|
-
'user_preferences',
|
|
356
|
-
`user_id=eq.${AMALGM_USER_ID}&select=preferences`,
|
|
357
|
-
);
|
|
358
|
-
if (!rows) return;
|
|
359
|
-
const preferences = rows?.[0]?.preferences;
|
|
360
|
-
const recentPrefs = preferences?.recent_prefs;
|
|
361
|
-
if (recentPrefs && typeof recentPrefs === 'object') {
|
|
362
|
-
for (const [h, m] of Object.entries(recentPrefs)) {
|
|
363
|
-
if (m) {
|
|
364
|
-
const normalized = normalizeModelSelection(h, m).modelId || m;
|
|
365
|
-
if (_selectedModels[h] !== normalized) _selectedModels[h] = normalized;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const usagePrefs = preferences?.usage;
|
|
370
|
-
if (usagePrefs && typeof usagePrefs === 'object') {
|
|
371
|
-
for (const [h, method] of Object.entries(usagePrefs)) {
|
|
372
|
-
if (method) _authMethods[h] = coerceAuthMethodForHarness(h, method);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
_pinnedHarness = preferences?.autopilot?.harness || null;
|
|
376
|
-
const rawPinnedModel = preferences?.autopilot?.model || null;
|
|
377
|
-
if (rawPinnedModel) {
|
|
378
|
-
const harnessForPinned =
|
|
379
|
-
_pinnedHarness ||
|
|
380
|
-
inferHarnessForModel(rawPinnedModel, preferences?.last_used?.harness || null);
|
|
381
|
-
_pinnedModel = harnessForPinned
|
|
382
|
-
? normalizeModelSelection(harnessForPinned, rawPinnedModel).modelId || rawPinnedModel
|
|
383
|
-
: rawPinnedModel;
|
|
384
|
-
} else {
|
|
385
|
-
_pinnedModel = null;
|
|
386
|
-
}
|
|
548
|
+
const preferences = readUserPreferences();
|
|
387
549
|
console.log(
|
|
388
550
|
'[AmalgmMCP] Hydrated user prefs:',
|
|
389
551
|
JSON.stringify({
|
|
390
552
|
selectedModels: _selectedModels,
|
|
391
553
|
authMethods: _authMethods,
|
|
392
|
-
|
|
393
|
-
pinnedModel: _pinnedModel,
|
|
554
|
+
lastUsed: preferences.last_used,
|
|
394
555
|
}),
|
|
395
556
|
);
|
|
396
557
|
} catch (err) {
|
|
@@ -403,7 +564,6 @@ async function hydrateModelPreferences() {
|
|
|
403
564
|
}
|
|
404
565
|
|
|
405
566
|
function getSelectedModel(harnessId) {
|
|
406
|
-
if (_pinnedHarness === harnessId && _pinnedModel) return _pinnedModel;
|
|
407
567
|
return _selectedModels[harnessId] || DEFAULT_SELECTED_MODELS[harnessId] || null;
|
|
408
568
|
}
|
|
409
569
|
|
|
@@ -425,16 +585,20 @@ function resolveModelSelection(harnessId, uiModelId) {
|
|
|
425
585
|
}
|
|
426
586
|
const normalized = normalizeModelSelection(harnessId, selectedModelId);
|
|
427
587
|
const modelId = normalized.modelId || selectedModelId;
|
|
588
|
+
const reasoningEffort = reasoningEffortForSelection(harnessId, modelId, normalized.reasoningEffort);
|
|
428
589
|
return {
|
|
429
590
|
modelId,
|
|
430
|
-
cliModel: resolveCliModel(harnessId, modelId),
|
|
431
|
-
reasoningEffort
|
|
591
|
+
cliModel: resolveCliModel(harnessId, modelId, reasoningEffort),
|
|
592
|
+
reasoningEffort,
|
|
432
593
|
};
|
|
433
594
|
}
|
|
434
595
|
|
|
435
596
|
module.exports = {
|
|
436
597
|
DEFAULT_SELECTED_MODELS,
|
|
437
598
|
hydrateModelPreferences,
|
|
599
|
+
readUserPreferences,
|
|
600
|
+
writeUserPreferences,
|
|
601
|
+
updateUserPreferences,
|
|
438
602
|
getSelectedModel,
|
|
439
603
|
getPreferredAuthMethod,
|
|
440
604
|
resolveModelSelection,
|