@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.
Files changed (59) hide show
  1. package/AGENTS.md +1 -0
  2. package/PURPOSE.md +44 -2
  3. package/README.md +10 -0
  4. package/dist/api/conversations.d.ts +7 -0
  5. package/dist/api/conversations.d.ts.map +1 -1
  6. package/dist/api/conversations.js +39 -1
  7. package/dist/api/conversations.js.map +1 -1
  8. package/dist/api/index.d.ts +1 -1
  9. package/dist/api/index.d.ts.map +1 -1
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/execution/contract.d.ts +1 -1
  12. package/dist/execution/contract.d.ts.map +1 -1
  13. package/dist/execution/index.d.ts +2 -0
  14. package/dist/execution/index.d.ts.map +1 -1
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/index.d.ts +3 -0
  21. package/dist/mcp/index.d.ts.map +1 -0
  22. package/dist/mcp/index.js +2 -0
  23. package/dist/mcp/index.js.map +1 -0
  24. package/dist/mcp/server.d.ts +7 -0
  25. package/dist/mcp/server.d.ts.map +1 -0
  26. package/dist/mcp/server.js +136 -0
  27. package/dist/mcp/server.js.map +1 -0
  28. package/dist/mcp/types.d.ts +17 -0
  29. package/dist/mcp/types.d.ts.map +1 -0
  30. package/dist/mcp/types.js +2 -0
  31. package/dist/mcp/types.js.map +1 -0
  32. package/dist/normalizers/claude.d.ts +46 -46
  33. package/dist/normalizers/codex.d.ts +23 -23
  34. package/docs/contracts/capabilities-and-instructions.md +103 -0
  35. package/docs/contracts/conversation-persistence.md +3 -0
  36. package/docs/contracts/input-and-execution.md +8 -3
  37. package/host/adapters/acp-capabilities.js +32 -0
  38. package/host/adapters/acp.js +8 -12
  39. package/host/adapters/claude.js +3 -2
  40. package/host/adapters/codex.js +19 -6
  41. package/host/adapters/cursor.js +9 -15
  42. package/host/adapters/input-capabilities.js +1 -0
  43. package/host/adapters/opencode.js +4 -3
  44. package/host/adapters/pi.js +3 -2
  45. package/host/adapters/prompt.js +2 -3
  46. package/host/auth.js +1 -1
  47. package/host/http.d.ts +4 -0
  48. package/host/http.js +43 -0
  49. package/host/index.d.ts +45 -3
  50. package/host/index.js +3 -0
  51. package/host/native-contract.js +26 -4
  52. package/host/native-runtime.js +8 -0
  53. package/host/platform-egress.js +46 -25
  54. package/host/title-generator.js +110 -0
  55. package/host/tooling/mcp-bundle.js +107 -56
  56. package/host/tooling/system-prompt.js +7 -4
  57. package/package.json +6 -1
  58. package/skills/chat/SKILL.md +300 -89
  59. 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
- /** Engine `runtime-auth.js getRuntimeToken`, byte-faithful. */
4
- function getRuntimeToken() {
5
- return String(process.env.AMALGM_RUNTIME_TOKEN || '').trim();
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 = safeName(server.name);
53
- const type = normalizeType(server.type);
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) throw new InvalidMcpServerError(`${name} stdio MCP server requires command`);
57
- const args = Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [];
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
- return { name, type, command, args, env };
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: Array.isArray(server.headers)
73
- ? server.headers
74
- : recordToHeaderArray(server.headers),
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
- const byName = new Map();
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.type === 'stdio'
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 = safeName(serverName);
113
- return mcpServers(contract).find((server) => server.name === clean && server.type !== 'stdio') || null;
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.type === 'stdio') {
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.type === 'stdio') {
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
- lines.push(`[mcp_servers.${server.name}]`);
172
- if (server.type === 'stdio') {
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(`[mcp_servers.${server.name}.env]`);
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.type,
193
- ...(server.type === 'stdio'
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.type === 'stdio'
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 once. */
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 = contract?.providerSessionId
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.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",