@amalgm/chat 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/PURPOSE.md +44 -2
- package/README.md +10 -0
- package/dist/api/conversations.d.ts +7 -0
- package/dist/api/conversations.d.ts.map +1 -1
- package/dist/api/conversations.js +39 -1
- package/dist/api/conversations.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/execution/contract.d.ts +1 -1
- package/dist/execution/contract.d.ts.map +1 -1
- package/dist/execution/index.d.ts +2 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +136 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/types.d.ts +17 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/normalizers/claude.d.ts +46 -46
- package/dist/normalizers/codex.d.ts +23 -23
- package/docs/contracts/capabilities-and-instructions.md +103 -0
- package/docs/contracts/conversation-persistence.md +3 -0
- package/docs/contracts/input-and-execution.md +8 -3
- package/host/adapters/acp-capabilities.js +32 -0
- package/host/adapters/acp.js +8 -12
- package/host/adapters/claude.js +3 -2
- package/host/adapters/codex.js +19 -6
- package/host/adapters/cursor.js +9 -15
- package/host/adapters/input-capabilities.js +1 -0
- package/host/adapters/opencode.js +4 -3
- package/host/adapters/pi.js +3 -2
- package/host/adapters/prompt.js +2 -3
- package/host/auth.js +1 -1
- package/host/http.d.ts +4 -0
- package/host/http.js +43 -0
- package/host/index.d.ts +45 -3
- package/host/index.js +3 -0
- package/host/native-contract.js +26 -4
- package/host/native-runtime.js +8 -0
- package/host/platform-egress.js +46 -25
- package/host/title-generator.js +110 -0
- package/host/tooling/mcp-bundle.js +107 -56
- package/host/tooling/system-prompt.js +7 -4
- package/package.json +6 -1
- package/skills/chat/SKILL.md +300 -89
- package/skills/chat/references/contracts.md +73 -47
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Exact prepared MCP-server projection for every native adapter. */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function getRuntimeToken(contract) {
|
|
6
|
+
return String(contract?.runtimeToken || '').trim();
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function headerArrayToRecord(headers) {
|
|
@@ -22,21 +23,6 @@ function recordToHeaderArray(headers) {
|
|
|
22
23
|
.map(([name, value]) => ({ name, value: String(value) }));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function safeName(name) {
|
|
26
|
-
return String(name || '')
|
|
27
|
-
.trim()
|
|
28
|
-
.replace(/[^A-Za-z0-9_.-]+/g, '_')
|
|
29
|
-
.replace(/^_+|_+$/g, '')
|
|
30
|
-
|| 'mcp';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeType(type) {
|
|
34
|
-
const clean = String(type || '').toLowerCase();
|
|
35
|
-
if (clean === 'sse') return 'sse';
|
|
36
|
-
if (clean === 'stdio') return 'stdio';
|
|
37
|
-
return 'http';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
export class InvalidMcpServerError extends Error {
|
|
41
27
|
constructor(message) {
|
|
42
28
|
super(message);
|
|
@@ -45,34 +31,98 @@ export class InvalidMcpServerError extends Error {
|
|
|
45
31
|
}
|
|
46
32
|
}
|
|
47
33
|
|
|
34
|
+
function exactName(value) {
|
|
35
|
+
const name = String(value || '').trim();
|
|
36
|
+
if (!name) throw new InvalidMcpServerError('ACP MCP server requires a non-empty name');
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function immutableClone(value) {
|
|
41
|
+
const clone = structuredClone(value);
|
|
42
|
+
const seen = new WeakSet();
|
|
43
|
+
const freeze = (item) => {
|
|
44
|
+
if (item == null || typeof item !== 'object' || seen.has(item)) return item;
|
|
45
|
+
seen.add(item);
|
|
46
|
+
for (const child of Object.values(item)) freeze(child);
|
|
47
|
+
return Object.freeze(item);
|
|
48
|
+
};
|
|
49
|
+
return freeze(clone);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clonedMeta(value) {
|
|
53
|
+
return Object.hasOwn(value, '_meta') ? { _meta: immutableClone(value._meta) } : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function entries(value, field) {
|
|
57
|
+
if (!Array.isArray(value)) {
|
|
58
|
+
throw new InvalidMcpServerError(`${field} must be an ACP name/value array`);
|
|
59
|
+
}
|
|
60
|
+
return value.map((entry) => {
|
|
61
|
+
const name = String(entry?.name || '').trim();
|
|
62
|
+
if (!name) throw new InvalidMcpServerError(`${field} has an empty name`);
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
name,
|
|
65
|
+
value: String(entry?.value ?? ''),
|
|
66
|
+
...clonedMeta(entry),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function meta(server) {
|
|
72
|
+
return clonedMeta(server);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function transport(server) {
|
|
76
|
+
return server.type === undefined ? 'stdio' : server.type;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
function normalizeServer(server) {
|
|
49
80
|
if (!server || typeof server !== 'object') {
|
|
50
81
|
throw new InvalidMcpServerError('MCP server must be an object');
|
|
51
82
|
}
|
|
52
|
-
const name =
|
|
53
|
-
|
|
54
|
-
if (type === 'stdio') {
|
|
83
|
+
const name = exactName(server.name);
|
|
84
|
+
if (server.type === undefined) {
|
|
55
85
|
const command = String(server.command || '').trim();
|
|
56
|
-
if (!command)
|
|
57
|
-
|
|
58
|
-
const env = Array.isArray(server.env)
|
|
59
|
-
? server.env.map((entry) => ({ name: String(entry?.name || ''), value: String(entry?.value || '') }))
|
|
60
|
-
: recordToHeaderArray(server.env);
|
|
61
|
-
if (env.some((entry) => !entry.name)) {
|
|
62
|
-
throw new InvalidMcpServerError(`${name} stdio MCP server has an empty environment variable name`);
|
|
86
|
+
if (!path.isAbsolute(command)) {
|
|
87
|
+
throw new InvalidMcpServerError(`${name} ACP stdio MCP server requires an absolute command`);
|
|
63
88
|
}
|
|
64
|
-
|
|
89
|
+
if (!Array.isArray(server.args)) {
|
|
90
|
+
throw new InvalidMcpServerError(`${name}.args must be an ACP string array`);
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
name,
|
|
94
|
+
command,
|
|
95
|
+
args: Object.freeze(server.args.map((arg) => String(arg))),
|
|
96
|
+
env: Object.freeze(entries(server.env, `${name}.env`)),
|
|
97
|
+
...meta(server),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (server.type !== 'http' && server.type !== 'sse') {
|
|
101
|
+
throw new InvalidMcpServerError(`${name} has unsupported ACP MCP transport: ${String(server.type)}`);
|
|
65
102
|
}
|
|
66
103
|
const url = String(server.url || '').trim();
|
|
67
|
-
if (!url) throw new InvalidMcpServerError(`${name} ${type} MCP server requires url`);
|
|
68
|
-
return {
|
|
104
|
+
if (!url) throw new InvalidMcpServerError(`${name} ${server.type} MCP server requires url`);
|
|
105
|
+
return Object.freeze({
|
|
69
106
|
name,
|
|
70
|
-
type,
|
|
107
|
+
type: server.type,
|
|
71
108
|
url,
|
|
72
|
-
headers:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
109
|
+
headers: Object.freeze(entries(server.headers, `${name}.headers`)),
|
|
110
|
+
...meta(server),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Validate and clone the official ACP McpServer union without widening it. */
|
|
115
|
+
export function normalizeAcpMcpServers(servers) {
|
|
116
|
+
if (!Array.isArray(servers)) throw new InvalidMcpServerError('MCP servers must be an ACP array');
|
|
117
|
+
const names = new Set();
|
|
118
|
+
return Object.freeze(servers.map((server) => {
|
|
119
|
+
const normalized = normalizeServer(server);
|
|
120
|
+
if (names.has(normalized.name)) {
|
|
121
|
+
throw new InvalidMcpServerError(`Duplicate ACP MCP server name: ${normalized.name}`);
|
|
122
|
+
}
|
|
123
|
+
names.add(normalized.name);
|
|
124
|
+
return normalized;
|
|
125
|
+
}));
|
|
76
126
|
}
|
|
77
127
|
|
|
78
128
|
function localBaseUrl(contract) {
|
|
@@ -82,13 +132,7 @@ function localBaseUrl(contract) {
|
|
|
82
132
|
}
|
|
83
133
|
|
|
84
134
|
export function mcpServers(contract) {
|
|
85
|
-
|
|
86
|
-
const add = (server) => {
|
|
87
|
-
const normalized = normalizeServer(server);
|
|
88
|
-
byName.set(normalized.name, normalized);
|
|
89
|
-
};
|
|
90
|
-
for (const server of Array.isArray(contract.mcpServers) ? contract.mcpServers : []) add(server);
|
|
91
|
-
return [...byName.values()];
|
|
135
|
+
return normalizeAcpMcpServers(contract.mcpServers || []);
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
function relayUrl(contract, serverName) {
|
|
@@ -96,8 +140,8 @@ function relayUrl(contract, serverName) {
|
|
|
96
140
|
}
|
|
97
141
|
|
|
98
142
|
export function relayedMcpServers(contract) {
|
|
99
|
-
const runtimeToken = getRuntimeToken();
|
|
100
|
-
return mcpServers(contract).map((server) => server
|
|
143
|
+
const runtimeToken = getRuntimeToken(contract);
|
|
144
|
+
return mcpServers(contract).map((server) => transport(server) === 'stdio'
|
|
101
145
|
? server
|
|
102
146
|
: ({
|
|
103
147
|
...server,
|
|
@@ -109,14 +153,14 @@ export function relayedMcpServers(contract) {
|
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
export function findMcpRelayTarget(contract, serverName) {
|
|
112
|
-
const clean =
|
|
113
|
-
return mcpServers(contract).find((server) => server.name === clean && server
|
|
156
|
+
const clean = String(serverName || '').trim();
|
|
157
|
+
return mcpServers(contract).find((server) => server.name === clean && transport(server) !== 'stdio') || null;
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
export function toClaudeMcpServers(contract) {
|
|
117
161
|
const out = {};
|
|
118
162
|
for (const server of relayedMcpServers(contract)) {
|
|
119
|
-
if (server
|
|
163
|
+
if (transport(server) === 'stdio') {
|
|
120
164
|
out[server.name] = {
|
|
121
165
|
type: 'stdio',
|
|
122
166
|
command: server.command,
|
|
@@ -138,7 +182,7 @@ export function toClaudeMcpServers(contract) {
|
|
|
138
182
|
export function toOpenCodeMcpConfig(contract) {
|
|
139
183
|
const out = {};
|
|
140
184
|
for (const server of relayedMcpServers(contract)) {
|
|
141
|
-
if (server
|
|
185
|
+
if (transport(server) === 'stdio') {
|
|
142
186
|
out[server.name] = {
|
|
143
187
|
type: 'local',
|
|
144
188
|
command: [server.command, ...server.args],
|
|
@@ -164,17 +208,22 @@ function tomlString(value) {
|
|
|
164
208
|
return JSON.stringify(String(value || ''));
|
|
165
209
|
}
|
|
166
210
|
|
|
211
|
+
export function codexMcpSectionName(name) {
|
|
212
|
+
return `mcp_servers.${tomlString(name)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
167
215
|
export function toCodexMcpToml(contract) {
|
|
168
216
|
const lines = [];
|
|
169
|
-
const runtimeToken = getRuntimeToken();
|
|
217
|
+
const runtimeToken = getRuntimeToken(contract);
|
|
170
218
|
for (const server of relayedMcpServers(contract)) {
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
const section = codexMcpSectionName(server.name);
|
|
220
|
+
lines.push(`[${section}]`);
|
|
221
|
+
if (transport(server) === 'stdio') {
|
|
173
222
|
lines.push(`command = ${tomlString(server.command)}`);
|
|
174
223
|
if (server.args.length) lines.push(`args = ${JSON.stringify(server.args)}`);
|
|
175
224
|
if (server.env.length) {
|
|
176
225
|
lines.push('');
|
|
177
|
-
lines.push(`[
|
|
226
|
+
lines.push(`[${section}.env]`);
|
|
178
227
|
for (const entry of server.env) lines.push(`${tomlString(entry.name)} = ${tomlString(entry.value)}`);
|
|
179
228
|
}
|
|
180
229
|
} else {
|
|
@@ -189,8 +238,8 @@ export function toCodexMcpToml(contract) {
|
|
|
189
238
|
export function mcpSummary(contract) {
|
|
190
239
|
return mcpServers(contract).map((server) => ({
|
|
191
240
|
name: server.name,
|
|
192
|
-
type: server
|
|
193
|
-
...(server
|
|
241
|
+
type: transport(server),
|
|
242
|
+
...(transport(server) === 'stdio'
|
|
194
243
|
? { command: server.command, hasEnvironment: server.env.length > 0 }
|
|
195
244
|
: { url: server.url, hasHeaders: server.headers.length > 0 }),
|
|
196
245
|
}));
|
|
@@ -198,17 +247,19 @@ export function mcpSummary(contract) {
|
|
|
198
247
|
|
|
199
248
|
/** Official ACP MCP-server projection used by Cursor and generic ACP agents. */
|
|
200
249
|
export function toAcpMcpServers(contract) {
|
|
201
|
-
return relayedMcpServers(contract).map((server) => server
|
|
250
|
+
return relayedMcpServers(contract).map((server) => transport(server) === 'stdio'
|
|
202
251
|
? {
|
|
203
252
|
name: server.name,
|
|
204
253
|
command: server.command,
|
|
205
254
|
args: [...server.args],
|
|
206
255
|
env: server.env.map((entry) => ({ ...entry })),
|
|
256
|
+
...(Object.hasOwn(server, '_meta') ? { _meta: structuredClone(server._meta) } : {}),
|
|
207
257
|
}
|
|
208
258
|
: {
|
|
209
259
|
type: server.type,
|
|
210
260
|
name: server.name,
|
|
211
261
|
url: server.url,
|
|
212
262
|
headers: server.headers.map((header) => ({ ...header })),
|
|
263
|
+
...(Object.hasOwn(server, '_meta') ? { _meta: structuredClone(server._meta) } : {}),
|
|
213
264
|
});
|
|
214
265
|
}
|
|
@@ -15,12 +15,10 @@ function safeBlock(label, render) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** Compose machine, project, and exact agent-revision instructions
|
|
18
|
+
/** Compose machine, project, and exact agent-revision instructions at preparation. */
|
|
19
19
|
export function composeSystemPrompt(contract, options = {}) {
|
|
20
20
|
const parts = [];
|
|
21
|
-
const systemInstructions =
|
|
22
|
-
? ''
|
|
23
|
-
: safeBlock('system instructions', systemInstructionsBlock);
|
|
21
|
+
const systemInstructions = safeBlock('system instructions', systemInstructionsBlock);
|
|
24
22
|
const projectContext = safeBlock(
|
|
25
23
|
'project context',
|
|
26
24
|
typeof options.projectContextPromptBlock === 'function'
|
|
@@ -33,3 +31,8 @@ export function composeSystemPrompt(contract, options = {}) {
|
|
|
33
31
|
if (agentInstructions) parts.push(agentInstructions);
|
|
34
32
|
return parts.join('\n\n');
|
|
35
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Read the already-prepared instruction bundle without performing I/O. */
|
|
36
|
+
export function preparedSystemPrompt(contract) {
|
|
37
|
+
return trimBlock(contract?.instructions?.text);
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amalgm/chat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A provider-agnostic agent chat SDK with ACP content, prepared execution, normalized streams, and usage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
"types": "./dist/conversations/index.d.ts",
|
|
57
57
|
"default": "./dist/conversations/index.js"
|
|
58
58
|
},
|
|
59
|
+
"./mcp": {
|
|
60
|
+
"types": "./dist/mcp/index.d.ts",
|
|
61
|
+
"default": "./dist/mcp/index.js"
|
|
62
|
+
},
|
|
59
63
|
"./sqlite": {
|
|
60
64
|
"types": "./host/sqlite/index.d.ts",
|
|
61
65
|
"default": "./host/sqlite/index.js"
|
|
@@ -70,6 +74,7 @@
|
|
|
70
74
|
"AGENTS.md",
|
|
71
75
|
"PURPOSE.md",
|
|
72
76
|
"docs/contracts/acp-and-step-usage.md",
|
|
77
|
+
"docs/contracts/capabilities-and-instructions.md",
|
|
73
78
|
"docs/contracts/input-and-execution.md",
|
|
74
79
|
"docs/contracts/conversation-persistence.md",
|
|
75
80
|
"docs/contracts/platform-authorization.md",
|