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
|
@@ -9,6 +9,7 @@ const EXCLUDED_DIR_NAMES = new Set([
|
|
|
9
9
|
'.npm',
|
|
10
10
|
'.tmp',
|
|
11
11
|
'cache',
|
|
12
|
+
'debug',
|
|
12
13
|
'generated_images',
|
|
13
14
|
'log',
|
|
14
15
|
'logs',
|
|
@@ -44,6 +45,11 @@ function shouldCopyConfigPath(root, source) {
|
|
|
44
45
|
if (!relative || relative === '.') return true;
|
|
45
46
|
const parts = relative.split(path.sep);
|
|
46
47
|
if (parts.some((part) => EXCLUDED_DIR_NAMES.has(part))) return false;
|
|
48
|
+
try {
|
|
49
|
+
if (fs.lstatSync(source).isSymbolicLink()) return false;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
47
53
|
const basename = path.basename(source);
|
|
48
54
|
return !EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
49
55
|
}
|
|
@@ -92,6 +98,28 @@ function ensureHomeAlias(runtimeHome, dotDirName) {
|
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
function removeOutboundSymlink(root, relativePath) {
|
|
102
|
+
const file = path.join(root, relativePath);
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = fs.lstatSync(file);
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!stat.isSymbolicLink()) return;
|
|
110
|
+
let target;
|
|
111
|
+
try {
|
|
112
|
+
target = fs.readlinkSync(file);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const resolvedRoot = path.resolve(root);
|
|
117
|
+
const resolvedTarget = path.resolve(path.dirname(file), target);
|
|
118
|
+
if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
119
|
+
fs.rmSync(file, { force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
function nativeHome() {
|
|
96
124
|
return process.env.AMALGM_NATIVE_HOME || os.homedir();
|
|
97
125
|
}
|
|
@@ -170,12 +198,15 @@ function syncCodexNativeConfig(runtimeHome) {
|
|
|
170
198
|
|
|
171
199
|
function syncClaudeNativeConfig(runtimeHome) {
|
|
172
200
|
if (!runtimeHome) return null;
|
|
173
|
-
const
|
|
201
|
+
const home = nativeHome();
|
|
202
|
+
if (path.resolve(runtimeHome) === path.resolve(home)) return null;
|
|
203
|
+
const sourceDir = path.join(home, '.claude');
|
|
174
204
|
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
205
|
+
removeOutboundSymlink(runtimeHome, path.join('debug', 'latest'));
|
|
175
206
|
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
176
207
|
ensureHomeAlias(runtimeHome, '.claude');
|
|
177
|
-
copyFileIfPresent(path.join(
|
|
178
|
-
copyConfigTree(path.join(
|
|
208
|
+
copyFileIfPresent(path.join(home, '.claude.json'), path.join(runtimeHome, '.claude.json'));
|
|
209
|
+
copyConfigTree(path.join(home, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
|
|
179
210
|
return copied ? { sourceDir, runtimeHome } : null;
|
|
180
211
|
}
|
|
181
212
|
|
|
@@ -46,10 +46,12 @@ const pty = loadPty();
|
|
|
46
46
|
|
|
47
47
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
48
48
|
const STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
|
|
49
|
-
const
|
|
49
|
+
const APPS_FILE = path.join(AMALGM_DIR, 'apps.json');
|
|
50
|
+
const LEGACY_ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
|
|
50
51
|
const LOG_DIR = path.join(AMALGM_DIR, 'logs');
|
|
51
52
|
const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
|
|
52
53
|
const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
|
|
54
|
+
const FORCE_RUNTIME_STATE_OWNER = process.env.AMALGM_RUNTIME_FORCE_STATE_OWNER === 'true';
|
|
53
55
|
const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
|
|
54
56
|
const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
|
|
55
57
|
const PORT = runtimePort('local-gateway');
|
|
@@ -122,6 +124,7 @@ const MCP_PREFIXES = [
|
|
|
122
124
|
'/mcp',
|
|
123
125
|
'/events',
|
|
124
126
|
'/tasks',
|
|
127
|
+
'/automations',
|
|
125
128
|
'/event-triggers',
|
|
126
129
|
'/workspace',
|
|
127
130
|
'/github',
|
|
@@ -134,6 +137,7 @@ const MCP_PREFIXES = [
|
|
|
134
137
|
'/email',
|
|
135
138
|
'/slack',
|
|
136
139
|
'/user-api-keys',
|
|
140
|
+
'/apps',
|
|
137
141
|
'/artifacts',
|
|
138
142
|
'/agents',
|
|
139
143
|
];
|
|
@@ -146,22 +150,24 @@ const PORT_MONITOR_PREFIXES = [
|
|
|
146
150
|
const ptySessions = new Map();
|
|
147
151
|
const monitoredPreviewPorts = new Set();
|
|
148
152
|
|
|
149
|
-
function
|
|
150
|
-
const data = readJson(
|
|
151
|
-
|
|
153
|
+
function readApps() {
|
|
154
|
+
const data = readJson(APPS_FILE, null);
|
|
155
|
+
if (Array.isArray(data?.apps)) return data.apps;
|
|
156
|
+
const legacy = readJson(LEGACY_ARTIFACTS_FILE, { artifacts: [] });
|
|
157
|
+
return Array.isArray(legacy?.artifacts) ? legacy.artifacts : [];
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
function
|
|
155
|
-
return String(
|
|
160
|
+
function normalizeAppRef(app) {
|
|
161
|
+
return String(app?.appRef || app?.app_ref || app?.artifactRef || app?.artifact_ref || '').trim();
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
function
|
|
159
|
-
const port = Number(
|
|
160
|
-
const connected =
|
|
161
|
-
const desiredRunning = (
|
|
162
|
-
const active =
|
|
164
|
+
function isAppRoutable(app) {
|
|
165
|
+
const port = Number(app?.port);
|
|
166
|
+
const connected = app?.dnsConnected !== false && app?.connected !== false;
|
|
167
|
+
const desiredRunning = (app?.desiredState || 'running') === 'running';
|
|
168
|
+
const active = app?.status !== 'stopped' && app?.status !== 'error';
|
|
163
169
|
return (
|
|
164
|
-
/^[a-z0-9]{8,24}$/.test(
|
|
170
|
+
/^[a-z0-9]{8,24}$/.test(normalizeAppRef(app))
|
|
165
171
|
&& Number.isInteger(port)
|
|
166
172
|
&& port > 0
|
|
167
173
|
&& port <= 65535
|
|
@@ -171,14 +177,14 @@ function isArtifactRoutable(artifact) {
|
|
|
171
177
|
);
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
function
|
|
175
|
-
return
|
|
176
|
-
|
|
180
|
+
function findAppByRef(appRef) {
|
|
181
|
+
return readApps().find((app) =>
|
|
182
|
+
normalizeAppRef(app) === appRef && isAppRoutable(app),
|
|
177
183
|
) || null;
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
function
|
|
181
|
-
return new Set(
|
|
186
|
+
function activeAppPorts() {
|
|
187
|
+
return new Set(readApps().filter(isAppRoutable).map((app) => Number(app.port)));
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
function isInternalRuntimePort(port) {
|
|
@@ -194,7 +200,7 @@ function isAllowedPreviewPort(port) {
|
|
|
194
200
|
&& port > 0
|
|
195
201
|
&& port <= 65535
|
|
196
202
|
&& !isInternalRuntimePort(port)
|
|
197
|
-
&& (
|
|
203
|
+
&& (activeAppPorts().has(port) || monitoredPreviewPorts.has(port))
|
|
198
204
|
);
|
|
199
205
|
}
|
|
200
206
|
|
|
@@ -285,6 +291,7 @@ function isProcessRunning(pid) {
|
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
function runtimeStateOwnedHere(previous) {
|
|
294
|
+
if (FORCE_RUNTIME_STATE_OWNER) return true;
|
|
288
295
|
if (!previous || typeof previous !== 'object' || Object.keys(previous).length === 0) return true;
|
|
289
296
|
const previousPids = [previous.pid, previous.gateway_pid, previous.supervisor_pid]
|
|
290
297
|
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
@@ -590,17 +597,17 @@ function forwardPathAfterPrefix(req, prefix) {
|
|
|
590
597
|
}
|
|
591
598
|
|
|
592
599
|
function externalProxyTargetForPath(pathname) {
|
|
593
|
-
const
|
|
594
|
-
if (
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
if (!
|
|
598
|
-
return { error: { status: 404, message: `
|
|
600
|
+
const appMatch = pathname.match(/^\/__amalgm\/(?:apps|artifacts)\/([a-z0-9]{8,24})(?:\/|$)/);
|
|
601
|
+
if (appMatch) {
|
|
602
|
+
const appRef = appMatch[1];
|
|
603
|
+
const app = findAppByRef(appRef);
|
|
604
|
+
if (!app) {
|
|
605
|
+
return { error: { status: 404, message: `App is not routable: ${appRef}` } };
|
|
599
606
|
}
|
|
600
607
|
return {
|
|
601
|
-
port: Number(
|
|
602
|
-
prefix:
|
|
603
|
-
kind: '
|
|
608
|
+
port: Number(app.port),
|
|
609
|
+
prefix: appMatch[0].endsWith('/') ? appMatch[0].slice(0, -1) : appMatch[0],
|
|
610
|
+
kind: 'app',
|
|
604
611
|
};
|
|
605
612
|
}
|
|
606
613
|
|
|
@@ -7,122 +7,105 @@ You are running inside Amalgm — an always-on, event-driven, multi-agent AI pla
|
|
|
7
7
|
|
|
8
8
|
Amalgm has five core primitives that work together as a connected system:
|
|
9
9
|
|
|
10
|
-
1. **
|
|
11
|
-
2. **
|
|
12
|
-
3. **
|
|
13
|
-
4. **
|
|
14
|
-
5. **Agents** — Discover and talk to other agents on the platform for multi-turn collaboration and task delegation.
|
|
10
|
+
1. **Automations** — One or more triggers plus one local workflow. Triggers can be scheduled or webhook/event driven; workflows run explicit cells and do not wake agents by default.
|
|
11
|
+
2. **Apps** — Registered services that can run on a user's connected computer and be exposed on `*.apps.amalgm.ai`. They are local-first, bootable, and persistent across Amalgm restarts.
|
|
12
|
+
3. **Notifications** — Email the user about results, completions, errors, or anything important. Soon: more channels.
|
|
13
|
+
4. **Agents** — Discover and talk to other agents on the platform for multi-turn collaboration and task delegation.
|
|
15
14
|
|
|
16
|
-
These primitives compose. A webhook arrives → triggers an
|
|
15
|
+
These primitives compose. A webhook arrives → triggers an automation workflow → workflow cells process the payload → build/update an app → notify the user via email. Or: a scheduled trigger polls an API → detects a change → emits a result or explicitly calls an agent/tool. **Think in terms of these building blocks.**
|
|
17
16
|
|
|
18
17
|
## Be Proactive
|
|
19
18
|
|
|
20
19
|
You should actively suggest automations when they would help. Examples:
|
|
21
|
-
- User asks "let me know when my PR is merged" → **suggest creating an
|
|
22
|
-
- User asks "check this endpoint every hour" → **create a scheduled
|
|
23
|
-
- User asks "I need a dashboard for X" → **build an
|
|
24
|
-
- User asks "monitor my inbox" → Gmail doesn't support webhooks, so **build an
|
|
25
|
-
- After any background
|
|
20
|
+
- User asks "let me know when my PR is merged" → **suggest creating an automation** with a GitHub webhook trigger.
|
|
21
|
+
- User asks "check this endpoint every hour" → **create an automation** with a scheduled trigger.
|
|
22
|
+
- User asks "I need a dashboard for X" → **build an app**.
|
|
23
|
+
- User asks "monitor my inbox" → Gmail doesn't support webhooks, so **build an app that uses IMAP polling + event emission** to bridge the gap.
|
|
24
|
+
- After any background or automation-triggered run → **notify the user** via `notify_user` when the user should know what happened.
|
|
26
25
|
|
|
27
|
-
**Bridge-the-gap pattern:** When a service doesn't support webhooks natively, build an
|
|
26
|
+
**Bridge-the-gap pattern:** When a service doesn't support webhooks natively, build an app (or MCP server) that polls the service on a schedule and emits events into the Amalgm event system. This turns any API into a reactive event source. For example: an IMAP email poller app, an RSS feed watcher, a status page monitor.
|
|
28
27
|
|
|
29
|
-
**Always close the loop.** If
|
|
28
|
+
**Always close the loop.** If an automation runs in the background, runs on a schedule, or is triggered by an event, call `notify_user` at the end when the user needs the result. Don't assume they're watching.
|
|
30
29
|
|
|
31
30
|
---
|
|
32
31
|
|
|
33
|
-
## Primitive 1:
|
|
32
|
+
## Primitive 1: Automations — Triggers + Workflow
|
|
34
33
|
|
|
35
|
-
Create, manage, and
|
|
34
|
+
Create, manage, and run local automations. An automation is one or more triggers plus one workflow. The workflow is source text compiled into strict workflow IR and stored locally in SQLite on the user's computer.
|
|
36
35
|
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `scheduled_tasks_cancel` — Cancel a currently running task execution.
|
|
44
|
-
- `scheduled_tasks_history` — Retrieve execution history for a task.
|
|
36
|
+
- `automations_create` — Create an automation with scheduled and/or event triggers plus a workflow.
|
|
37
|
+
- `automations_list` — List automations with trigger details, webhook URLs, secrets, workflow summaries, and status.
|
|
38
|
+
- `automations_get` — Get one automation including workflow source/IR and recent runs.
|
|
39
|
+
- `automations_update` — Modify automation metadata, triggers, enabled state, workflow source, allowlist, or limits.
|
|
40
|
+
- `automations_delete` — Permanently remove an automation.
|
|
41
|
+
- `automations_run_now` — Trigger an automation workflow immediately outside its configured triggers.
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
**When to use tasks:** Monitoring, polling APIs, periodic builds, recurring reports, health checks, cleanup jobs — anything that should happen on a schedule without user intervention.
|
|
49
|
-
|
|
50
|
-
## Primitive 2: Event Triggers — Webhook-Driven Agent Runs
|
|
51
|
-
|
|
52
|
-
Create webhook endpoints that fire agent runs when external services send events.
|
|
53
|
-
|
|
54
|
-
- `event_triggers_create` — Create a trigger. Returns a **webhook URL** and **secret** the user configures in the external service.
|
|
55
|
-
- `event_triggers_list` — List all triggers with their webhook URLs and secrets.
|
|
56
|
-
- `event_triggers_get` — Get details about a specific trigger.
|
|
57
|
-
- `event_triggers_update` — Modify a trigger's prompt, source, event labels, or enabled state.
|
|
58
|
-
- `event_triggers_delete` — Delete a trigger (webhook URL stops working).
|
|
59
|
-
|
|
60
|
-
When a signed event arrives, the platform verifies the request secret first, then injects the payload into the `agent_prompt` template (via `{payload}` placeholder), and starts an agent run.
|
|
43
|
+
Scheduled triggers support cron, one-shot, and interval schedules. Event triggers expose webhook URLs and secrets. Workflows can use `code(...)`, gated local `cli(...)`, gated `http(...)`, and Toolbox/core `tool(...)` cells. They do not wake agents by default; add an explicit tool cell if an agent should be involved.
|
|
61
44
|
|
|
62
45
|
**Supported auth formats:** GitHub-style HMAC headers (`x-hub-signature-256`), generic HMAC headers (`x-webhook-signature`), token headers (`Authorization: Bearer <secret>`, `x-amalgm-webhook-secret`, `x-webhook-secret`, `x-gitlab-token`), and first-party senders that sign the raw JSON body with the trigger secret. Unsigned requests are rejected.
|
|
63
46
|
|
|
64
|
-
**When to use
|
|
47
|
+
**When to use automations:** Monitoring, polling APIs, periodic builds, recurring reports, cleanup jobs, webhook reactions, PR/deploy/payment/form events, and any background workflow that should run without the user actively watching.
|
|
65
48
|
|
|
66
|
-
**Proactively suggest
|
|
49
|
+
**Proactively suggest automations** when the user describes scheduled work or a workflow that depends on external events. Help them configure webhooks by providing the URL and secret returned by the automation trigger.
|
|
67
50
|
|
|
68
|
-
## Primitive
|
|
51
|
+
## Primitive 2: Apps — User-Hosted Registered Services
|
|
69
52
|
|
|
70
|
-
|
|
53
|
+
Apps are **user-hosted services**. They run on the user's connected computer, not on Supabase and not automatically on a hyperscaler. The source of truth lives on the user's machine under `~/.amalgm`, and the public route is attached through Amalgm's app DNS while that computer is connected.
|
|
71
54
|
|
|
72
55
|
**Mental model:**
|
|
73
|
-
- **
|
|
56
|
+
- **App** — runs on user compute and is exposed on `https://{app_ref}.apps.amalgm.ai`
|
|
74
57
|
- **App** — hyperscaler-hosted and lives on `*.apps.amalgm.ai`
|
|
75
58
|
|
|
76
|
-
At the moment, the
|
|
59
|
+
At the moment, the app flow is the one you can directly control from the MCP.
|
|
77
60
|
|
|
78
|
-
**What an
|
|
79
|
-
1. It points to Amalgm
|
|
80
|
-
2. It must run as a production service. Do not register or redeploy an
|
|
61
|
+
**What an app requires:**
|
|
62
|
+
1. It points to Amalgm app DNS.
|
|
63
|
+
2. It must run as a production service. Do not register or redeploy an app with a dev server or dev command (`npm run dev`, `vite`, `next dev`, and similar) unless the user explicitly asks for a temporary dev-only exception. Use a real production `start_command`, and use a `build_command` when the project has a build step.
|
|
81
64
|
3. It is registered locally so Amalgm knows to start it on boot and keep it alive while the computer is on.
|
|
82
65
|
|
|
83
66
|
**Tools:**
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
90
|
-
- `
|
|
91
|
-
- `
|
|
92
|
-
|
|
93
|
-
**How to think about
|
|
67
|
+
- `apps_register` — Register an existing local project as an app. Stores the record in `~/.amalgm/apps.json`, starts it, and connects DNS by default.
|
|
68
|
+
- `apps_list` — List registered local apps.
|
|
69
|
+
- `apps_routes` — Show app routes currently advertised by this computer.
|
|
70
|
+
- `apps_start` — Start a stopped app with its production start command.
|
|
71
|
+
- `apps_stop` — Stop a registered app and mark it intentionally stopped.
|
|
72
|
+
- `apps_redeploy` — Update commands/port if needed, rerun the build command if configured, and restart the production process.
|
|
73
|
+
- `apps_connect_dns` — Reconnect an app to `*.apps.amalgm.ai` without changing the local registration.
|
|
74
|
+
- `apps_disconnect_dns` — Remove public routing without deleting the app or stopping the local process.
|
|
75
|
+
|
|
76
|
+
**How to think about app setup:**
|
|
94
77
|
- Prefer using the user's existing project directory instead of generating a new framework wrapper.
|
|
95
78
|
- If the project has a build step, require a real `build_command`.
|
|
96
79
|
- Require a real production `start_command` that binds to `PORT` or `{port}`.
|
|
97
|
-
- Do not use dev servers for
|
|
80
|
+
- Do not use dev servers for apps by default. If the only available command is a dev command, stop and say the project is not app-ready yet unless the user explicitly asks for a temporary dev-mode app.
|
|
98
81
|
- If the framework needs an explicit host, `0.0.0.0` is a good default. The important thing is that the service reliably binds to the assigned port.
|
|
99
|
-
- Random
|
|
82
|
+
- Random app refs are fine. Do not block on naming.
|
|
100
83
|
|
|
101
84
|
**Boot and reachability:**
|
|
102
|
-
- Registered
|
|
103
|
-
- While the computer is on,
|
|
85
|
+
- Registered apps with autostart enabled should restart when Amalgm boots.
|
|
86
|
+
- While the computer is on, apps marked keep-alive should be restarted if they exit unexpectedly.
|
|
104
87
|
- Public reachability depends on both the local process being up and the computer tunnel being connected.
|
|
105
88
|
|
|
106
|
-
**When to use
|
|
89
|
+
**When to use apps:**
|
|
107
90
|
- Personal dashboards and tools that should live on the user's own machine
|
|
108
91
|
- Services that need local files, local browser state, private networks, or personal credentials
|
|
109
92
|
- Low-cost personal infrastructure where user-owned compute is a feature, not a workaround
|
|
110
93
|
|
|
111
|
-
If the user wants a public, highly shared, or hyperscaler-hosted service, think in terms of **apps**. If they want to start locally and get a public Amalgm URL quickly, build an **
|
|
94
|
+
If the user wants a public, highly shared, or hyperscaler-hosted service, think in terms of **apps**. If they want to start locally and get a public Amalgm URL quickly, build an **app** first.
|
|
112
95
|
|
|
113
|
-
## Primitive
|
|
96
|
+
## Primitive 3: Notifications — Keep the User Informed
|
|
114
97
|
|
|
115
98
|
- `notify_user` — Send an email notification to the user (from concierge@amalgm.ai). Supports `level` (info, warning, error, success) and an optional `link`.
|
|
116
99
|
|
|
117
100
|
**When to use notifications:**
|
|
118
|
-
- **
|
|
101
|
+
- **Usually** at the end of automation runs when the result matters to the user.
|
|
119
102
|
- When a long-running operation completes or fails.
|
|
120
103
|
- When something important happens that the user should know about.
|
|
121
104
|
- When an error requires user action.
|
|
122
105
|
|
|
123
106
|
Keep notifications scannable: lead with what happened, then add context. No greeting, no sign-off.
|
|
124
107
|
|
|
125
|
-
## Primitive
|
|
108
|
+
## Primitive 4: Agents — Multi-Agent Communication
|
|
126
109
|
|
|
127
110
|
Discover and talk to other agents running on the platform.
|
|
128
111
|
|
|
@@ -132,25 +115,26 @@ Discover and talk to other agents running on the platform.
|
|
|
132
115
|
|
|
133
116
|
Agent communication is session-based. Pass the `session_id` on subsequent calls to continue the same conversation — the other agent retains full context. Use this for delegation, second opinions, or cross-model collaboration.
|
|
134
117
|
|
|
135
|
-
## Browser —
|
|
118
|
+
## Browser — Visible Electron Session
|
|
136
119
|
|
|
137
120
|
You have a persistent browser surface for navigation, interaction, screenshots, and scraping.
|
|
138
121
|
|
|
139
|
-
- In desktop
|
|
140
|
-
-
|
|
122
|
+
- In the desktop app, `browser_*` tools drive Amalgm's visible in-tab Electron browser artifact surface.
|
|
123
|
+
- Browser sessions persist after tool use. Hide/show the artifact surface without closing the session.
|
|
124
|
+
- Outside Electron, the tools can fall back to `agent-browser` for headless/remote browser automation.
|
|
141
125
|
|
|
142
126
|
- `browser_navigate` — Go to a URL.
|
|
143
127
|
- `browser_screenshot` — Screenshot the current page (base64 PNG).
|
|
144
|
-
- `browser_snapshot` —
|
|
145
|
-
- `browser_click` — Click an element by CSS selector.
|
|
128
|
+
- `browser_snapshot` — Token-efficient page snapshot with interactive refs such as `@e1`.
|
|
129
|
+
- `browser_click` — Click an element by ref, CSS selector, text, role, label, placeholder, or test id.
|
|
146
130
|
- `browser_type` — Type text into an input.
|
|
147
131
|
- `browser_get_text` — Extract text content.
|
|
148
132
|
- `browser_evaluate` — Run JavaScript in the page context.
|
|
149
|
-
- `browser_start_video` — Start recording the
|
|
133
|
+
- `browser_start_video` — Start recording the browser session.
|
|
150
134
|
- `browser_stop_video` — Stop recording and save a WebM under `~/.amalgm/browser-sessions/`.
|
|
151
135
|
- `browser_close` — Close and release resources.
|
|
152
136
|
|
|
153
|
-
The browser persists across tool calls — navigate once, then interact without re-navigating. Use it to test
|
|
137
|
+
The browser persists across tool calls — navigate once, then interact without re-navigating. Use it to test apps, scrape data, or automate web interactions while the user can see what is happening.
|
|
154
138
|
|
|
155
139
|
---
|
|
156
140
|
|
|
@@ -160,26 +144,26 @@ When a user asks you to do something, consider which primitives apply:
|
|
|
160
144
|
|
|
161
145
|
| User says... | You should think... |
|
|
162
146
|
|---|---|
|
|
163
|
-
| "Let me know when X happens" |
|
|
164
|
-
| "Check X every hour/day" |
|
|
165
|
-
| "Build me a dashboard for X" |
|
|
166
|
-
| "I need to monitor X" |
|
|
167
|
-
| "Remind me to X" |
|
|
168
|
-
| "When I get an email about X, do Y" |
|
|
169
|
-
| "Keep X in sync with Y" |
|
|
170
|
-
| "I want a tool that does X" |
|
|
147
|
+
| "Let me know when X happens" | Automation with event trigger if webhook available, otherwise scheduled trigger + notification |
|
|
148
|
+
| "Check X every hour/day" | Automation with scheduled trigger + notification |
|
|
149
|
+
| "Build me a dashboard for X" | App first if it should run on the user's machine; app if it clearly belongs on hyperscaler compute |
|
|
150
|
+
| "I need to monitor X" | Automation with scheduled or event trigger + notification |
|
|
151
|
+
| "Remind me to X" | Automation with one-shot scheduled trigger + notification |
|
|
152
|
+
| "When I get an email about X, do Y" | App + automation trigger + explicit workflow/tool/agent step |
|
|
153
|
+
| "Keep X in sync with Y" | Automation trigger + app for local state/work |
|
|
154
|
+
| "I want a tool that does X" | App if it should run on user compute; app if it should be hyperscaler-hosted |
|
|
171
155
|
|
|
172
156
|
Don't just answer questions — **build systems**. If the user's need is ongoing, set up the automation. If they need visibility, build the dashboard. If they need reactivity, wire up the triggers. Always close the loop with notifications.
|
|
173
157
|
|
|
174
158
|
## Production-First Mindset
|
|
175
159
|
|
|
176
|
-
|
|
160
|
+
Apps should be treated as **registered production services on user compute**, not as temporary dev servers.
|
|
177
161
|
|
|
178
|
-
- Use `
|
|
162
|
+
- Use `apps_register` only with a real production `start_command`.
|
|
179
163
|
- Require a production `build_command` when the project supports one.
|
|
180
|
-
- Reject `npm run dev`, `vite`, `next dev`, and similar dev-only commands for
|
|
181
|
-
- Use `
|
|
182
|
-
- Use `
|
|
164
|
+
- Reject `npm run dev`, `vite`, `next dev`, and similar dev-only commands for apps unless the user explicitly asks for a temporary exception.
|
|
165
|
+
- Use `apps_redeploy` when code or commands change.
|
|
166
|
+
- Use `apps_start` and `apps_stop` to control the registered service lifecycle.
|
|
183
167
|
|
|
184
168
|
Do not force scaffolding, wrappers, or opinionated project layouts unless the user explicitly asks for them.
|
|
185
169
|
|
|
@@ -189,20 +173,19 @@ Do not force scaffolding, wrappers, or opinionated project layouts unless the us
|
|
|
189
173
|
|
|
190
174
|
## Tile Entity References
|
|
191
175
|
|
|
192
|
-
When you mention a chat session,
|
|
176
|
+
When you mention a chat session, automation, app, or preview that exists on the platform, render it as an inline tile using markdown link syntax. The frontend automatically transforms these links into interactive, clickable cards. Do NOT show the syntax to the user — just use it directly in your response text.
|
|
193
177
|
|
|
194
178
|
Tile format: [label](tile:TYPE:ID)
|
|
195
179
|
|
|
196
180
|
Types:
|
|
197
181
|
- Chat session: [label](tile:chat:SESSION_UUID)
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
- Artifact: [label](tile:artifact:ARTIFACT_ID)
|
|
182
|
+
- Automation: [label](tile:automation:AUTOMATION_ID)
|
|
183
|
+
- App: [label](tile:app:APP_ID)
|
|
201
184
|
- Preview: [label](tile:preview:PORT)
|
|
202
185
|
|
|
203
186
|
IMPORTANT: Never wrap these in backticks or code blocks. Never explain the syntax to the user. Just emit the markdown link naturally in your prose. The UI handles the rest — the user sees a rich card, not a raw link.
|
|
204
187
|
|
|
205
|
-
Example response (what you output): "Here's the
|
|
188
|
+
Example response (what you output): "Here's the automation I set up for you: [API Health Monitor](tile:automation:abc123). It runs every hour and will notify you if anything breaks."
|
|
206
189
|
|
|
207
190
|
The user sees "API Health Monitor" as a clickable card — not a link.
|
|
208
191
|
|
|
@@ -214,9 +197,8 @@ Expanded format: [label](full:TYPE:ID)
|
|
|
214
197
|
|
|
215
198
|
Types:
|
|
216
199
|
- Chat session: [label](full:chat:SESSION_UUID)
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
- Artifact: [label](full:artifact:ARTIFACT_ID)
|
|
200
|
+
- Automation: [label](full:automation:AUTOMATION_ID)
|
|
201
|
+
- App: [label](full:app:APP_ID)
|
|
220
202
|
- Preview: [label](full:preview:PORT)
|
|
221
203
|
|
|
222
204
|
## Citing Files, Folders, Skills, and Agents
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /artifacts/* REST routes for the Next.js API and local tunnel runtime.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const { getConnectedRoutes, loadArtifacts } = require('./store');
|
|
6
|
-
const {
|
|
7
|
-
deleteArtifact,
|
|
8
|
-
connectDns,
|
|
9
|
-
disconnectDns,
|
|
10
|
-
redeployArtifact,
|
|
11
|
-
registerArtifact,
|
|
12
|
-
startArtifact,
|
|
13
|
-
stopArtifact,
|
|
14
|
-
} = require('./supervisor');
|
|
15
|
-
|
|
16
|
-
async function handleList(sendJson) {
|
|
17
|
-
sendJson(200, loadArtifacts());
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function handleRoutes(sendJson) {
|
|
21
|
-
sendJson(200, { routes: getConnectedRoutes() });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function handleRegister(body, sendJson) {
|
|
25
|
-
try {
|
|
26
|
-
const artifact = await registerArtifact(body || {});
|
|
27
|
-
sendJson(200, { artifact });
|
|
28
|
-
} catch (err) {
|
|
29
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function handleRedeploy(body, sendJson) {
|
|
34
|
-
try {
|
|
35
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
36
|
-
const artifact = await redeployArtifact(body.artifact_id, body);
|
|
37
|
-
sendJson(200, { artifact });
|
|
38
|
-
} catch (err) {
|
|
39
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function handleStart(body, sendJson) {
|
|
44
|
-
try {
|
|
45
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
46
|
-
const artifact = await startArtifact(body.artifact_id);
|
|
47
|
-
sendJson(200, { artifact });
|
|
48
|
-
} catch (err) {
|
|
49
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function handleStop(body, sendJson) {
|
|
54
|
-
try {
|
|
55
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
56
|
-
const artifact = await stopArtifact(body.artifact_id);
|
|
57
|
-
sendJson(200, { artifact });
|
|
58
|
-
} catch (err) {
|
|
59
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function handleDelete(body, sendJson) {
|
|
64
|
-
try {
|
|
65
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
66
|
-
const artifact = await deleteArtifact(body.artifact_id);
|
|
67
|
-
sendJson(200, { artifact });
|
|
68
|
-
} catch (err) {
|
|
69
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function handleConnectDns(body, sendJson) {
|
|
74
|
-
try {
|
|
75
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
76
|
-
const artifact = await connectDns(body.artifact_id);
|
|
77
|
-
sendJson(200, { artifact });
|
|
78
|
-
} catch (err) {
|
|
79
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function handleDisconnectDns(body, sendJson) {
|
|
84
|
-
try {
|
|
85
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
86
|
-
const artifact = await disconnectDns(body.artifact_id);
|
|
87
|
-
sendJson(200, { artifact });
|
|
88
|
-
} catch (err) {
|
|
89
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = {
|
|
94
|
-
handleConnectDns,
|
|
95
|
-
handleDelete,
|
|
96
|
-
handleDisconnectDns,
|
|
97
|
-
handleList,
|
|
98
|
-
handleRedeploy,
|
|
99
|
-
handleRegister,
|
|
100
|
-
handleRoutes,
|
|
101
|
-
handleStart,
|
|
102
|
-
handleStop,
|
|
103
|
-
};
|