amalgm 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1000 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +467 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +29 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +306 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +128 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +138 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
- package/runtime/scripts/chat-core/adapters/claude.js +163 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +326 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +195 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +129 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +852 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<amalgm-platform>
|
|
2
|
+
# Amalgm Platform
|
|
3
|
+
|
|
4
|
+
You are running inside Amalgm — an always-on, event-driven, multi-agent AI platform. You are not a chatbot. You are a **resident agent** with a persistent container that runs 24/7. You have durable state, long-running capabilities, and the ability to act autonomously — even when no user is present.
|
|
5
|
+
|
|
6
|
+
## What Makes Amalgm Different
|
|
7
|
+
|
|
8
|
+
Amalgm has five core primitives that work together as a connected system:
|
|
9
|
+
|
|
10
|
+
1. **Tasks** — Scheduled and recurring work that runs autonomously on cron, interval, or one-shot schedules.
|
|
11
|
+
2. **Event Triggers** — Webhook endpoints that fire agent runs when external events arrive (GitHub pushes, Stripe payments, form submissions, etc.).
|
|
12
|
+
3. **Artifacts** — Registered services that can run on a user's connected computer and be exposed on `*.artifacts.amalgm.ai`. They are local-first, bootable, and persistent across Amalgm restarts.
|
|
13
|
+
4. **Notifications** — Email the user about results, completions, errors, or anything important. Soon: more channels.
|
|
14
|
+
5. **Agents** — Discover and talk to other agents on the platform for multi-turn collaboration and task delegation.
|
|
15
|
+
|
|
16
|
+
These primitives compose. A webhook arrives → triggers an agent run → the agent processes the payload → builds/updates an artifact → notifies the user via email. Or: a scheduled task polls an API → detects a change → emits an event → another agent handles it. **Think in terms of these building blocks.**
|
|
17
|
+
|
|
18
|
+
## Be Proactive
|
|
19
|
+
|
|
20
|
+
You should actively suggest automations when they would help. Examples:
|
|
21
|
+
- User asks "let me know when my PR is merged" → **suggest creating an event trigger** with a GitHub webhook.
|
|
22
|
+
- User asks "check this endpoint every hour" → **create a scheduled task**.
|
|
23
|
+
- User asks "I need a dashboard for X" → **build an artifact**.
|
|
24
|
+
- User asks "monitor my inbox" → Gmail doesn't support webhooks, so **build an artifact that uses IMAP polling + event emission** to bridge the gap.
|
|
25
|
+
- After any background/scheduled/event-triggered run → **notify the user** via `notify_user` so they know what happened.
|
|
26
|
+
|
|
27
|
+
**Bridge-the-gap pattern:** When a service doesn't support webhooks natively, build an artifact (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 artifact, an RSS feed watcher, a status page monitor.
|
|
28
|
+
|
|
29
|
+
**Always close the loop.** If a task runs in the background, runs on a schedule, or is triggered by an event — call `notify_user` at the end so the user knows what happened. Don't assume they're watching.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Primitive 1: Tasks — Scheduled & Recurring Work
|
|
34
|
+
|
|
35
|
+
Create, manage, and trigger scheduled tasks that execute prompts autonomously.
|
|
36
|
+
|
|
37
|
+
- `scheduled_tasks_create` — Create a task with a cron, once, or interval schedule.
|
|
38
|
+
- `scheduled_tasks_list` — List all tasks and their current status.
|
|
39
|
+
- `scheduled_tasks_get` — Get details on a specific task, including recent run logs.
|
|
40
|
+
- `scheduled_tasks_update` — Modify a task's schedule, prompt, enabled state, or concurrency limits.
|
|
41
|
+
- `scheduled_tasks_delete` — Permanently remove a task.
|
|
42
|
+
- `scheduled_tasks_run_now` — Trigger a task immediately, outside its schedule.
|
|
43
|
+
- `scheduled_tasks_cancel` — Cancel a currently running task execution.
|
|
44
|
+
- `scheduled_tasks_history` — Retrieve execution history for a task.
|
|
45
|
+
|
|
46
|
+
Tasks can run indefinitely (cron), a fixed number of times, or once. They can have end dates (`endsAt`). They support concurrency limits. They can target specific agents and models.
|
|
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.
|
|
61
|
+
|
|
62
|
+
**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
|
+
|
|
64
|
+
**When to use event triggers:** React to external events in real-time — PR merged, payment received, form submitted, deploy completed, issue created. Always prefer event triggers over polling when the external service supports webhooks.
|
|
65
|
+
|
|
66
|
+
**Proactively suggest event triggers** when the user describes a workflow that depends on external events. Help them configure the webhook in the external service by providing the URL and secret.
|
|
67
|
+
|
|
68
|
+
## Primitive 3: Artifacts — User-Hosted Registered Services
|
|
69
|
+
|
|
70
|
+
Artifacts 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 artifact DNS while that computer is connected.
|
|
71
|
+
|
|
72
|
+
**Mental model:**
|
|
73
|
+
- **Artifact** — runs on user compute and is exposed on `https://{artifact_ref}.artifacts.amalgm.ai`
|
|
74
|
+
- **App** — hyperscaler-hosted and lives on `*.apps.amalgm.ai`
|
|
75
|
+
|
|
76
|
+
At the moment, the artifact flow is the one you can directly control from the MCP.
|
|
77
|
+
|
|
78
|
+
**What an artifact requires:**
|
|
79
|
+
1. It points to Amalgm artifact DNS.
|
|
80
|
+
2. It must run as a production service. Do not register or redeploy an artifact 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
|
+
3. It is registered locally so Amalgm knows to start it on boot and keep it alive while the computer is on.
|
|
82
|
+
|
|
83
|
+
**Tools:**
|
|
84
|
+
- `artifacts_register` — Register an existing local project as an artifact. Stores the record in `~/.amalgm/artifacts.json`, starts it, and connects DNS by default.
|
|
85
|
+
- `artifacts_list` — List registered local artifacts.
|
|
86
|
+
- `artifacts_routes` — Show artifact routes currently advertised by this computer.
|
|
87
|
+
- `artifacts_start` — Start a stopped artifact with its production start command.
|
|
88
|
+
- `artifacts_stop` — Stop a registered artifact and mark it intentionally stopped.
|
|
89
|
+
- `artifacts_redeploy` — Update commands/port if needed, rerun the build command if configured, and restart the production process.
|
|
90
|
+
- `artifacts_connect_dns` — Reconnect an artifact to `*.artifacts.amalgm.ai` without changing the local registration.
|
|
91
|
+
- `artifacts_disconnect_dns` — Remove public routing without deleting the artifact or stopping the local process.
|
|
92
|
+
|
|
93
|
+
**How to think about artifact setup:**
|
|
94
|
+
- Prefer using the user's existing project directory instead of generating a new framework wrapper.
|
|
95
|
+
- If the project has a build step, require a real `build_command`.
|
|
96
|
+
- Require a real production `start_command` that binds to `PORT` or `{port}`.
|
|
97
|
+
- Do not use dev servers for artifacts by default. If the only available command is a dev command, stop and say the project is not artifact-ready yet unless the user explicitly asks for a temporary dev-mode artifact.
|
|
98
|
+
- 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 artifact refs are fine. Do not block on naming.
|
|
100
|
+
|
|
101
|
+
**Boot and reachability:**
|
|
102
|
+
- Registered artifacts with autostart enabled should restart when Amalgm boots.
|
|
103
|
+
- While the computer is on, artifacts marked keep-alive should be restarted if they exit unexpectedly.
|
|
104
|
+
- Public reachability depends on both the local process being up and the computer tunnel being connected.
|
|
105
|
+
|
|
106
|
+
**When to use artifacts:**
|
|
107
|
+
- Personal dashboards and tools that should live on the user's own machine
|
|
108
|
+
- Services that need local files, local browser state, private networks, or personal credentials
|
|
109
|
+
- Low-cost personal infrastructure where user-owned compute is a feature, not a workaround
|
|
110
|
+
|
|
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 **artifact** first.
|
|
112
|
+
|
|
113
|
+
## Primitive 4: Notifications — Keep the User Informed
|
|
114
|
+
|
|
115
|
+
- `notify_user` — Send an email notification to the user (from concierge@amalgm.ai). Supports `level` (info, warning, error, success) and an optional `link`.
|
|
116
|
+
|
|
117
|
+
**When to use notifications:**
|
|
118
|
+
- **Always** at the end of scheduled task runs and event-triggered runs.
|
|
119
|
+
- When a long-running operation completes or fails.
|
|
120
|
+
- When something important happens that the user should know about.
|
|
121
|
+
- When an error requires user action.
|
|
122
|
+
|
|
123
|
+
Keep notifications scannable: lead with what happened, then add context. No greeting, no sign-off.
|
|
124
|
+
|
|
125
|
+
## Primitive 5: Agents — Multi-Agent Communication
|
|
126
|
+
|
|
127
|
+
Discover and talk to other agents running on the platform.
|
|
128
|
+
|
|
129
|
+
- `agents_list` — List all available agents (built-in + custom).
|
|
130
|
+
- `agents_get` — Get details about a specific agent.
|
|
131
|
+
- `talk_to_agent` — Start or continue a conversation with another agent. Returns a `session_id` for multi-turn threads.
|
|
132
|
+
|
|
133
|
+
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
|
+
|
|
135
|
+
## Browser — Shared Chromium Session
|
|
136
|
+
|
|
137
|
+
You have a persistent browser surface for navigation, interaction, screenshots, and scraping.
|
|
138
|
+
|
|
139
|
+
- In desktop local mode, `browser_*` tools use a visible Electron browser tab with an on-page agent cursor, so user logins and cookies carry across tool calls and screenshots/videos include the cursor naturally.
|
|
140
|
+
- In non-desktop/container contexts, `browser_*` tools fall back to a private headless Chromium instance via Playwright.
|
|
141
|
+
|
|
142
|
+
- `browser_navigate` — Go to a URL.
|
|
143
|
+
- `browser_screenshot` — Screenshot the current page (base64 PNG).
|
|
144
|
+
- `browser_snapshot` — Accessibility tree snapshot (more token-efficient than screenshots).
|
|
145
|
+
- `browser_click` — Click an element by CSS selector.
|
|
146
|
+
- `browser_type` — Type text into an input.
|
|
147
|
+
- `browser_get_text` — Extract text content.
|
|
148
|
+
- `browser_evaluate` — Run JavaScript in the page context.
|
|
149
|
+
- `browser_start_video` — Start recording the visible browser session.
|
|
150
|
+
- `browser_stop_video` — Stop recording and save a WebM under `~/.amalgm/browser-sessions/`.
|
|
151
|
+
- `browser_close` — Close and release resources.
|
|
152
|
+
|
|
153
|
+
The browser persists across tool calls — navigate once, then interact without re-navigating. Use it to test artifacts, scrape data, or automate web interactions.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## How to Think About User Requests
|
|
158
|
+
|
|
159
|
+
When a user asks you to do something, consider which primitives apply:
|
|
160
|
+
|
|
161
|
+
| User says... | You should think... |
|
|
162
|
+
|---|---|
|
|
163
|
+
| "Let me know when X happens" | Event trigger (if webhook available) or polling task + notification |
|
|
164
|
+
| "Check X every hour/day" | Scheduled task + notification |
|
|
165
|
+
| "Build me a dashboard for X" | Artifact first if it should run on the user's machine; app if it clearly belongs on hyperscaler compute |
|
|
166
|
+
| "I need to monitor X" | Task (polling) or event trigger (webhook) + notification |
|
|
167
|
+
| "Remind me to X" | One-shot task + notification |
|
|
168
|
+
| "When I get an email about X, do Y" | Artifact + task/event trigger + agent run |
|
|
169
|
+
| "Keep X in sync with Y" | Event trigger or polling task + artifact for local state/work |
|
|
170
|
+
| "I want a tool that does X" | Artifact if it should run on user compute; app if it should be hyperscaler-hosted |
|
|
171
|
+
|
|
172
|
+
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
|
+
|
|
174
|
+
## Production-First Mindset
|
|
175
|
+
|
|
176
|
+
Artifacts should be treated as **registered production services on user compute**, not as temporary dev servers.
|
|
177
|
+
|
|
178
|
+
- Use `artifacts_register` only with a real production `start_command`.
|
|
179
|
+
- Require a production `build_command` when the project supports one.
|
|
180
|
+
- Reject `npm run dev`, `vite`, `next dev`, and similar dev-only commands for artifacts unless the user explicitly asks for a temporary exception.
|
|
181
|
+
- Use `artifacts_redeploy` when code or commands change.
|
|
182
|
+
- Use `artifacts_start` and `artifacts_stop` to control the registered service lifecycle.
|
|
183
|
+
|
|
184
|
+
Do not force scaffolding, wrappers, or opinionated project layouts unless the user explicitly asks for them.
|
|
185
|
+
|
|
186
|
+
### Network Rules
|
|
187
|
+
- ALWAYS bind to 0.0.0.0 (not localhost) — the container's localhost is not externally accessible.
|
|
188
|
+
- Use `wait-for-port <port> [timeout]` to block until a port is listening (default 30s timeout).
|
|
189
|
+
|
|
190
|
+
## Tile Entity References
|
|
191
|
+
|
|
192
|
+
When you mention a chat session, task, event trigger, artifact, 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
|
+
|
|
194
|
+
Tile format: [label](tile:TYPE:ID)
|
|
195
|
+
|
|
196
|
+
Types:
|
|
197
|
+
- Chat session: [label](tile:chat:SESSION_UUID)
|
|
198
|
+
- Task: [label](tile:task:TASK_ID)
|
|
199
|
+
- Event trigger: [label](tile:event:TRIGGER_ID)
|
|
200
|
+
- Artifact: [label](tile:artifact:ARTIFACT_ID)
|
|
201
|
+
- Preview: [label](tile:preview:PORT)
|
|
202
|
+
|
|
203
|
+
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
|
+
|
|
205
|
+
Example response (what you output): "Here's the task I set up for you: [API Health Monitor](tile:task:abc123). It runs every hour and will notify you if anything breaks."
|
|
206
|
+
|
|
207
|
+
The user sees "API Health Monitor" as a clickable card — not a link.
|
|
208
|
+
|
|
209
|
+
## Expanded Entity References
|
|
210
|
+
|
|
211
|
+
When a full in-chat preview is useful, render the entity with the expanded form. Expanded references use the message width, have a capped height, and include top-right actions for opening the entity in a tab.
|
|
212
|
+
|
|
213
|
+
Expanded format: [label](full:TYPE:ID)
|
|
214
|
+
|
|
215
|
+
Types:
|
|
216
|
+
- Chat session: [label](full:chat:SESSION_UUID)
|
|
217
|
+
- Task: [label](full:task:TASK_ID)
|
|
218
|
+
- Event trigger: [label](full:event:TRIGGER_ID)
|
|
219
|
+
- Artifact: [label](full:artifact:ARTIFACT_ID)
|
|
220
|
+
- Preview: [label](full:preview:PORT)
|
|
221
|
+
|
|
222
|
+
## Citing Files, Folders, Skills, and Agents
|
|
223
|
+
|
|
224
|
+
When you reference a file, folder, skill, or agent that exists on the user's local volume, emit it as an `amalgm://` markdown link. The frontend renders these as inline citation chips by default, with a hover card showing the path and (for skills) description. Skills are files too: if you only have the `SKILL.md` path, a normal file citation is valid. This is the same format the user's `@`-menu produces, and it's how agents talk to each other about local resources.
|
|
225
|
+
|
|
226
|
+
Render format: `[label](amalgm://KIND/PATH_OR_ID)`
|
|
227
|
+
|
|
228
|
+
Kinds:
|
|
229
|
+
- File: `[Component.tsx](amalgm://file//workspace/app/Component.tsx)`
|
|
230
|
+
- Folder: `[components](amalgm://folder//workspace/app/components)`
|
|
231
|
+
- Skill: `[email-formatter](amalgm://skill//workspace/.agents/skills/email-formatter "Formats outgoing emails with consistent voice")`
|
|
232
|
+
- Agent: `[Helper](amalgm://agent/AGENT_ID)`
|
|
233
|
+
|
|
234
|
+
For skills, append a one-line description as the markdown link title (the part in quotes after the URL). The description teaches downstream agents and the user what the skill does so they can decide whether to load it. Keep the description under ~140 chars.
|
|
235
|
+
|
|
236
|
+
For files and folders, the path alone is enough — no description needed. The user (and any agent reading your output) sees a chip; clicking it opens the file or folder.
|
|
237
|
+
|
|
238
|
+
IMPORTANT:
|
|
239
|
+
- Use the absolute path the user gave you. Don't rewrite, normalize, or shorten it.
|
|
240
|
+
- Don't wrap citations in backticks. Don't explain the syntax.
|
|
241
|
+
- When the user `@`-mentions something in their prompt, that input arrives as the same `amalgm://` link format — you can echo it back unchanged when referring to that resource.
|
|
242
|
+
|
|
243
|
+
Example response (what you output): "I updated the [chat-server](amalgm://folder//workspace/scripts/chat-server) entrypoint and pulled in the [retry-policy](amalgm://skill//workspace/.agents/skills/retry-policy "Adds bounded exponential backoff to flaky outbound calls") skill for the new HTTP client."
|
|
244
|
+
|
|
245
|
+
The user sees two clickable chips inline; the agent reading your transcript sees structured references it can act on.
|
|
246
|
+
</amalgm-platform>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Monitor - Standalone SSE server for port change events.
|
|
3
|
+
*
|
|
4
|
+
* Polls `ss -tlnp` every 2s and pushes
|
|
5
|
+
* port_opened / port_closed events over SSE.
|
|
6
|
+
*
|
|
7
|
+
* Runs alongside chat-server on a separate port (8081).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const { authorizeRuntimeHttp } = require('./runtime-auth');
|
|
14
|
+
|
|
15
|
+
const PORT = parseInt(process.env.PORT_MONITOR_PORT || '8081', 10);
|
|
16
|
+
const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
|
|
17
|
+
const AGENT_PORT = parseInt(process.env.PORT || '8080');
|
|
18
|
+
const FS_WATCHER_PORT = parseInt(process.env.FS_WATCHER_PORT || '8082', 10);
|
|
19
|
+
const AMALGM_MCP_PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
|
|
20
|
+
const CHAT_SERVER_PORT = parseInt(process.env.CHAT_SERVER_PORT || '8084', 10);
|
|
21
|
+
const AMALGM_GATEWAY_PORT = parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
|
|
22
|
+
const LOCAL_AGENT_APP_PORT = parseInt(process.env.AMALGM_LOCAL_APP_PORT || '3456', 10);
|
|
23
|
+
const INTERNAL_SERVICE_PORTS = new Set([
|
|
24
|
+
AGENT_PORT,
|
|
25
|
+
LOCAL_AGENT_APP_PORT,
|
|
26
|
+
PORT,
|
|
27
|
+
FS_WATCHER_PORT,
|
|
28
|
+
AMALGM_MCP_PORT,
|
|
29
|
+
CHAT_SERVER_PORT,
|
|
30
|
+
AMALGM_GATEWAY_PORT,
|
|
31
|
+
4096,
|
|
32
|
+
]);
|
|
33
|
+
const COMMON_DEV_PORTS = new Set([3000, 3001, 3002, 3003, 4000, 4200, 4321, 5000, 5001, 5173, 5174, 6006, 7000, 8000, 8001, 8888]);
|
|
34
|
+
|
|
35
|
+
const knownPorts = new Map();
|
|
36
|
+
const subscribers = new Set();
|
|
37
|
+
|
|
38
|
+
function readProcTcpPorts(file) {
|
|
39
|
+
try {
|
|
40
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
41
|
+
return text
|
|
42
|
+
.split('\n')
|
|
43
|
+
.slice(1)
|
|
44
|
+
.map((line) => line.trim().split(/\s+/))
|
|
45
|
+
.filter((parts) => parts.length > 3 && parts[3] === '0A')
|
|
46
|
+
.map((parts) => {
|
|
47
|
+
const localAddress = parts[1] || '';
|
|
48
|
+
const portHex = localAddress.split(':').pop();
|
|
49
|
+
return portHex ? parseInt(portHex, 16) : NaN;
|
|
50
|
+
})
|
|
51
|
+
.filter(Number.isFinite);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getListeningPortsFromProc() {
|
|
58
|
+
return [
|
|
59
|
+
...readProcTcpPorts('/proc/net/tcp'),
|
|
60
|
+
...readProcTcpPorts('/proc/net/tcp6'),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getListeningPorts() {
|
|
65
|
+
if (process.platform === 'darwin') {
|
|
66
|
+
const output = execSync('lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null', { encoding: 'utf-8' });
|
|
67
|
+
const ports = new Map();
|
|
68
|
+
for (const line of output.split('\n').slice(1)) {
|
|
69
|
+
const match = line.trim().match(/^(\S+)\s+\d+\s+.+\sTCP\s+.+:(\d+)\s+\(LISTEN\)$/);
|
|
70
|
+
if (match) ports.set(parseInt(match[2], 10), match[1] || null);
|
|
71
|
+
}
|
|
72
|
+
return ports;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const output = execSync('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null', { encoding: 'utf-8' });
|
|
77
|
+
const ports = new Map();
|
|
78
|
+
for (const line of output.split('\n')) {
|
|
79
|
+
const portMatch = line.match(/:(\d+)\s+/);
|
|
80
|
+
if (!portMatch) continue;
|
|
81
|
+
const procMatch = line.match(/users:\(\("([^"]+)"/) || line.match(/\/([^\s/]+)\s*$/);
|
|
82
|
+
ports.set(parseInt(portMatch[1], 10), procMatch ? procMatch[1] : null);
|
|
83
|
+
}
|
|
84
|
+
if (ports.size > 0) return ports;
|
|
85
|
+
} catch {
|
|
86
|
+
// Minimal Docker images often omit ss and netstat.
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Map(getListeningPortsFromProc().map((port) => [port, null]));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPreviewablePort(port, processName) {
|
|
93
|
+
if (!Number.isInteger(port) || port < 3000 || port > 65535) return false;
|
|
94
|
+
if (INTERNAL_SERVICE_PORTS.has(port)) return false;
|
|
95
|
+
if (port >= 9100 && port <= 9199) return false;
|
|
96
|
+
const name = String(processName || '').trim().toLowerCase();
|
|
97
|
+
if (['.opencode', 'controlce', 'figma_age', 'figma_agent', 'loom', 'opencode', 'rapportd'].includes(name)) return false;
|
|
98
|
+
if (name.startsWith('figma')) return false;
|
|
99
|
+
if (!name) return COMMON_DEV_PORTS.has(port);
|
|
100
|
+
if (/^(node|bun|deno|npm|pnpm|yarn|vite|next|astro|nuxt|svelte|remix|webpack|rspack|parcel|serve|http-server|tsx|python|python3|uvicorn|gunicorn|flask|django|ruby|rails|puma|php|java|go|air|cargo|trunk|dotnet|nginx)$/i.test(name)) return true;
|
|
101
|
+
return COMMON_DEV_PORTS.has(port) && name === 'unknown';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkPorts() {
|
|
105
|
+
try {
|
|
106
|
+
const ports = getListeningPorts();
|
|
107
|
+
for (const [port, processName] of ports) {
|
|
108
|
+
if (!isPreviewablePort(port, processName)) ports.delete(port);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const [port, processName] of ports) {
|
|
112
|
+
if (!knownPorts.has(port)) {
|
|
113
|
+
knownPorts.set(port, processName);
|
|
114
|
+
broadcast({ type: 'port_opened', port, processName, timestamp: Date.now() });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const [port] of knownPorts) {
|
|
119
|
+
if (!ports.has(port)) {
|
|
120
|
+
knownPorts.delete(port);
|
|
121
|
+
broadcast({ type: 'port_closed', port, timestamp: Date.now() });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function broadcast(event) {
|
|
128
|
+
const data = `event: port\ndata: ${JSON.stringify(event)}\n\n`;
|
|
129
|
+
for (const res of subscribers) {
|
|
130
|
+
try { res.write(data); } catch (e) { subscribers.delete(res); }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setInterval(checkPorts, 2000);
|
|
135
|
+
setTimeout(checkPorts, 1000);
|
|
136
|
+
|
|
137
|
+
const server = http.createServer((req, res) => {
|
|
138
|
+
const url = new URL(req.url, 'http://localhost:' + PORT);
|
|
139
|
+
|
|
140
|
+
if (!authorizeRuntimeHttp(req, res, { methods: 'GET, OPTIONS' })) return;
|
|
141
|
+
|
|
142
|
+
if (url.pathname === '/healthz' || url.pathname === '/') {
|
|
143
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({
|
|
145
|
+
status: 'ok',
|
|
146
|
+
activePorts: [...knownPorts.entries()].map(([port, processName]) => ({ port, processName })),
|
|
147
|
+
}));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (url.pathname === '/api/ports/subscribe') {
|
|
152
|
+
res.writeHead(200, {
|
|
153
|
+
'Content-Type': 'text/event-stream',
|
|
154
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
155
|
+
'Connection': 'keep-alive',
|
|
156
|
+
});
|
|
157
|
+
res.write(`event: init\ndata: ${JSON.stringify({ ports: [...knownPorts.entries()].map(([port, processName]) => ({ port, processName })) })}\n\n`);
|
|
158
|
+
subscribers.add(res);
|
|
159
|
+
req.on('close', () => subscribers.delete(res));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (url.pathname === '/api/ports') {
|
|
164
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
165
|
+
res.end(JSON.stringify({ ports: [...knownPorts.entries()].map(([port, processName]) => ({ port, processName })) }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
res.writeHead(404);
|
|
170
|
+
res.end('Not found');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
server.listen(PORT, BIND_HOST, () => {
|
|
174
|
+
console.log('[PortMonitor] Listening on ' + BIND_HOST + ':' + PORT);
|
|
175
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PROXY_BASE_URL = 'https://amalgm-api-proxy-v2.fly.dev';
|
|
8
|
+
const REFRESH_BUFFER_MS = 10 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
11
|
+
const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
|
|
12
|
+
const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
|
|
13
|
+
|
|
14
|
+
let refreshPromise = null;
|
|
15
|
+
|
|
16
|
+
function readJson(file, fallback = null) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeJsonSecret(file, data) {
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
26
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
27
|
+
fs.writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
fs.renameSync(temp, file);
|
|
29
|
+
try {
|
|
30
|
+
fs.chmodSync(file, 0o600);
|
|
31
|
+
} catch {
|
|
32
|
+
// chmod is best-effort on non-POSIX filesystems.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanString(value) {
|
|
37
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadAuthContext() {
|
|
41
|
+
const record = readJson(COMPUTER_FILE, null) || {};
|
|
42
|
+
const auth = readJson(AUTH_FILE, null) || {};
|
|
43
|
+
if (record.computer_id && auth.computer_id && record.computer_id !== auth.computer_id) {
|
|
44
|
+
return { record, auth: {} };
|
|
45
|
+
}
|
|
46
|
+
return { record, auth };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function proxyRefreshContext(record, auth) {
|
|
50
|
+
return {
|
|
51
|
+
machineToken:
|
|
52
|
+
cleanString(auth?.machine?.token)
|
|
53
|
+
|| cleanString(auth?.computer_auth_token)
|
|
54
|
+
|| cleanString(auth?.tunnel?.token),
|
|
55
|
+
appUrl: cleanString(record?.app_url) || cleanString(auth?.app_url),
|
|
56
|
+
computerId: cleanString(record?.computer_id) || cleanString(auth?.computer_id),
|
|
57
|
+
deviceId: cleanString(record?.device_id) || cleanString(auth?.device_id),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function canRefreshProxyToken(context = loadAuthContext()) {
|
|
62
|
+
const refresh = proxyRefreshContext(context.record, context.auth);
|
|
63
|
+
return !!(refresh.machineToken && refresh.appUrl && refresh.computerId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readProxyToken(context = loadAuthContext()) {
|
|
67
|
+
const envToken = cleanString(process.env.AMALGM_PROXY_TOKEN);
|
|
68
|
+
return cleanString(context.auth?.proxy?.token) || envToken;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function proxyTokenExpiresSoon(auth) {
|
|
72
|
+
if (!cleanString(auth?.proxy?.token)) return false;
|
|
73
|
+
const expiresAtRaw = cleanString(auth?.proxy?.expires_at);
|
|
74
|
+
if (!expiresAtRaw) return true;
|
|
75
|
+
const expiresAt = Date.parse(expiresAtRaw);
|
|
76
|
+
if (!Number.isFinite(expiresAt)) return true;
|
|
77
|
+
return expiresAt - Date.now() < REFRESH_BUFFER_MS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function proxyBaseUrl() {
|
|
81
|
+
const context = loadAuthContext();
|
|
82
|
+
return cleanString(process.env.AMALGM_PROXY_URL)
|
|
83
|
+
|| (readProxyToken(context) || canRefreshProxyToken(context) ? DEFAULT_PROXY_BASE_URL : '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function refreshProxyToken() {
|
|
87
|
+
const { record, auth } = loadAuthContext();
|
|
88
|
+
const {
|
|
89
|
+
machineToken,
|
|
90
|
+
appUrl,
|
|
91
|
+
computerId,
|
|
92
|
+
deviceId,
|
|
93
|
+
} = proxyRefreshContext(record, auth);
|
|
94
|
+
|
|
95
|
+
if (!machineToken || !appUrl || !computerId) return readProxyToken();
|
|
96
|
+
|
|
97
|
+
const response = await fetch(`${appUrl.replace(/\/$/, '')}/api/computers/proxy-token`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
authorization: `Bearer ${machineToken}`,
|
|
101
|
+
'content-type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
computer_id: computerId,
|
|
105
|
+
device_id: deviceId,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const text = await response.text().catch(() => '');
|
|
111
|
+
throw new Error(`proxy token refresh failed: ${response.status} ${text.slice(0, 200)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const refreshed = await response.json();
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
const nextAuth = {
|
|
117
|
+
...auth,
|
|
118
|
+
schema_version: 1,
|
|
119
|
+
computer_id: computerId,
|
|
120
|
+
device_id: deviceId,
|
|
121
|
+
app_url: appUrl,
|
|
122
|
+
proxy: {
|
|
123
|
+
...(auth.proxy || {}),
|
|
124
|
+
token: cleanString(refreshed.proxy_token) || cleanString(auth.proxy?.token),
|
|
125
|
+
expires_in: Number(refreshed.proxy_token_expires_in || auth.proxy?.expires_in || 0) || undefined,
|
|
126
|
+
expires_at: cleanString(refreshed.proxy_token_expires_at) || cleanString(auth.proxy?.expires_at),
|
|
127
|
+
refreshed_at: now,
|
|
128
|
+
},
|
|
129
|
+
updated_at: now,
|
|
130
|
+
};
|
|
131
|
+
writeJsonSecret(AUTH_FILE, nextAuth);
|
|
132
|
+
return cleanString(nextAuth.proxy.token);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function ensureFreshProxyToken(options = {}) {
|
|
136
|
+
const force = options.force === true;
|
|
137
|
+
const context = loadAuthContext();
|
|
138
|
+
const token = readProxyToken(context);
|
|
139
|
+
if (token && !force && !proxyTokenExpiresSoon(context.auth)) return token;
|
|
140
|
+
if (!canRefreshProxyToken(context)) return token;
|
|
141
|
+
|
|
142
|
+
if (!refreshPromise) {
|
|
143
|
+
refreshPromise = refreshProxyToken().finally(() => {
|
|
144
|
+
refreshPromise = null;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return await refreshPromise;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const logger = options.logger || console;
|
|
152
|
+
logger.warn?.(`[proxy-token] ${error.message || error}`);
|
|
153
|
+
return token;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
DEFAULT_PROXY_BASE_URL,
|
|
159
|
+
ensureFreshProxyToken,
|
|
160
|
+
proxyBaseUrl,
|
|
161
|
+
readProxyToken,
|
|
162
|
+
};
|