artifacty 0.10.0 → 0.10.2

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.
@@ -130,9 +130,29 @@ their password before they can continue to `/account` and issue MCP/API tokens.
130
130
  - `--api-token <token>` is written into the generated MCP environment for bridge mode. For central servers, prefer a personal token issued from `/account`.
131
131
  - `--url <url>` remains a browser-link override and does not enable central MCP by itself.
132
132
  - `--timeout <ms>` adjusts Codex `startup_timeout_sec` and Gemini `timeout`. It does not change Claude Code startup behavior; set `MCP_TIMEOUT` before launching Claude Code if you need a larger value there.
133
+ - Do not use `artifacty install all --config <path>`. Each MCP client uses a
134
+ different config file shape, so shared config overrides are rejected. Use
135
+ `artifacty install <agent> --config <path>` only when targeting one agent.
136
+ - JSON-based installers preserve unrelated servers and replace only the
137
+ `artifacty` entry. If `mcpServers` or `servers` already exists but is not a
138
+ JSON object, Artifacty stops with an error instead of rewriting the file.
133
139
  - `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
134
140
  - `doctor` combines MCP discovery with runtime, storage, server, and service diagnostics.
135
141
 
142
+ Codex troubleshooting:
143
+
144
+ - If Codex reports a duplicate `mcp_servers.artifacty.env` or `artifacty.env`
145
+ key, update Artifacty and rerun the Codex installer. Older installs could
146
+ leave a legacy `[mcp_servers.artifacty.env]` child table behind while adding a
147
+ new inline `env = { ... }` value.
148
+
149
+ ```bash
150
+ npm install -g artifacty@latest
151
+ artifacty install codex \
152
+ --mcp-url http://10.0.0.50:8787/mcp \
153
+ --api-token "$ARTIFACTY_PERSONAL_TOKEN"
154
+ ```
155
+
136
156
  ## Client Compatibility Matrix
137
157
 
138
158
  | Client | Config shape | Scope | Timeout behavior | Restart requirement | Notes |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/cli.js CHANGED
@@ -2,42 +2,24 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import {
6
- archiveArtifact,
7
- checkStoreIntegrity,
8
- createArtifact,
9
- createStore,
10
- getArtifact,
11
- importUsersFromCsv,
12
- listAuditEvents,
13
- listArtifactsPage,
14
- rebuildSearchIndex,
15
- restoreArtifact,
16
- updateArtifact
17
- } from "./lib/storage.js";
18
- import { exportStore, importStore, defaultBackupPath } from "./lib/backup.js";
19
- import { convertAgentArtifact } from "./lib/converters.js";
20
- import { checkMcpTools } from "./lib/check.js";
21
- import { installAgent } from "./lib/installer.js";
22
- import { serviceCommand } from "./lib/service.js";
23
- import { backgroundStatus, startBackgroundServer, stopBackgroundServer } from "./lib/background.js";
24
- import { resolvePublicBaseUrl } from "./lib/server-state.js";
25
5
  import { generateToken } from "./lib/token.js";
26
- import { runDoctor } from "./lib/doctor.js";
27
- import { startServer } from "./server.js";
28
6
 
29
7
  const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
30
8
 
31
9
  async function main() {
32
10
  const [command, ...args] = process.argv.slice(2);
33
11
  const options = parseArgs(args);
34
- const store = createStore({ home: options.home });
35
12
 
36
13
  if (!command || command === "help" || command === "--help" || command === "-h") {
37
14
  printHelp();
38
15
  return;
39
16
  }
40
17
 
18
+ if (command === "version" || command === "--version" || command === "-v") {
19
+ process.stdout.write(`${await packageVersion()}\n`);
20
+ return;
21
+ }
22
+
41
23
  if (command === "token" || command === "generate-token") {
42
24
  const token = generateToken(options);
43
25
  if (options.raw) {
@@ -48,6 +30,29 @@ async function main() {
48
30
  return;
49
31
  }
50
32
 
33
+ const {
34
+ archiveArtifact,
35
+ checkStoreIntegrity,
36
+ createArtifact,
37
+ createStore,
38
+ getArtifact,
39
+ importUsersFromCsv,
40
+ listAuditEvents,
41
+ listArtifactsPage,
42
+ rebuildSearchIndex,
43
+ restoreArtifact,
44
+ updateArtifact
45
+ } = await import("./lib/storage.js");
46
+ const { exportStore, importStore, defaultBackupPath } = await import("./lib/backup.js");
47
+ const { convertAgentArtifact } = await import("./lib/converters.js");
48
+ const { checkMcpTools } = await import("./lib/check.js");
49
+ const { installAgent } = await import("./lib/installer.js");
50
+ const { serviceCommand } = await import("./lib/service.js");
51
+ const { backgroundStatus, startBackgroundServer, stopBackgroundServer } = await import("./lib/background.js");
52
+ const { runDoctor } = await import("./lib/doctor.js");
53
+ const { startServer } = await import("./server.js");
54
+ const store = createStore({ home: options.home });
55
+
51
56
  if (command === "serve") {
52
57
  if (options.detach && options.foreground) {
53
58
  throw new Error("Use either --foreground or --detach, not both");
@@ -412,6 +417,7 @@ function shouldReadFileAsBase64(options, filePath) {
412
417
  }
413
418
 
414
419
  async function withUrls(store, artifact) {
420
+ const { resolvePublicBaseUrl } = await import("./lib/server-state.js");
415
421
  const publicBaseUrl = await resolvePublicBaseUrl(store);
416
422
  return {
417
423
  ...artifact,
@@ -442,10 +448,16 @@ function printJson(data) {
442
448
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
443
449
  }
444
450
 
451
+ async function packageVersion() {
452
+ const packageJson = JSON.parse(await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
453
+ return packageJson.version;
454
+ }
455
+
445
456
  function printHelp() {
446
457
  process.stdout.write(`Artifacty
447
458
 
448
459
  Usage:
460
+ artifacty --version
449
461
  artifacty token [--bytes 32] [--raw]
450
462
  artifacty serve [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--generate-token] [--bytes 32] [--mcp-http] [--foreground]
451
463
  artifacty serve --foreground [--generate-token]
@@ -14,6 +14,9 @@ export async function installAgent(agent, options = {}) {
14
14
  }
15
15
 
16
16
  if (normalized === "all") {
17
+ if (options.configPath) {
18
+ throw new Error("install all does not support --config because each MCP client uses a different config file. Run install per agent when overriding config paths.");
19
+ }
17
20
  const results = [];
18
21
  for (const target of INSTALL_TARGETS) {
19
22
  results.push(await installAgent(target, options));
@@ -73,11 +76,12 @@ export function createMcpServerConfig(options = {}) {
73
76
  export async function installClaude(options = {}) {
74
77
  const projectDir = path.resolve(options.projectDir || process.cwd());
75
78
  const targetPath = path.resolve(options.configPath || path.join(projectDir, ".mcp.json"));
76
- const existing = await readJsonFile(targetPath, {});
79
+ const existing = await readJsonConfigFile(targetPath);
80
+ const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
77
81
  const next = {
78
82
  ...existing,
79
83
  mcpServers: {
80
- ...(existing.mcpServers || {}),
84
+ ...mcpServers,
81
85
  artifacty: createMcpServerConfig(options)
82
86
  }
83
87
  };
@@ -93,11 +97,12 @@ export async function installClaude(options = {}) {
93
97
  export async function installGemini(options = {}) {
94
98
  const projectDir = path.resolve(options.projectDir || process.cwd());
95
99
  const targetPath = path.resolve(options.configPath || path.join(projectDir, ".gemini", "settings.json"));
96
- const existing = await readJsonFile(targetPath, {});
100
+ const existing = await readJsonConfigFile(targetPath);
101
+ const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
97
102
  const next = {
98
103
  ...existing,
99
104
  mcpServers: {
100
- ...(existing.mcpServers || {}),
105
+ ...mcpServers,
101
106
  artifacty: {
102
107
  ...createMcpServerConfig(options),
103
108
  timeout: normalizeTimeoutMs(options.timeout),
@@ -117,11 +122,12 @@ export async function installGemini(options = {}) {
117
122
  export async function installCopilot(options = {}) {
118
123
  const projectDir = path.resolve(options.projectDir || process.cwd());
119
124
  const targetPath = path.resolve(options.configPath || path.join(projectDir, ".vscode", "mcp.json"));
120
- const existing = await readJsonFile(targetPath, {});
125
+ const existing = await readJsonConfigFile(targetPath);
126
+ const servers = jsonObjectSection(existing, "servers", targetPath);
121
127
  const next = {
122
128
  ...existing,
123
129
  servers: {
124
- ...(existing.servers || {}),
130
+ ...servers,
125
131
  artifacty: {
126
132
  type: "stdio",
127
133
  ...createMcpServerConfig(options)
@@ -140,11 +146,12 @@ export async function installCopilot(options = {}) {
140
146
  export async function installCursor(options = {}) {
141
147
  const projectDir = path.resolve(options.projectDir || process.cwd());
142
148
  const targetPath = path.resolve(options.configPath || path.join(projectDir, ".cursor", "mcp.json"));
143
- const existing = await readJsonFile(targetPath, {});
149
+ const existing = await readJsonConfigFile(targetPath);
150
+ const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
144
151
  const next = {
145
152
  ...existing,
146
153
  mcpServers: {
147
- ...(existing.mcpServers || {}),
154
+ ...mcpServers,
148
155
  artifacty: createMcpServerConfig(options)
149
156
  }
150
157
  };
@@ -190,11 +197,35 @@ export function codexTomlBlock(config, options = {}) {
190
197
  }
191
198
 
192
199
  export function replaceTomlBlock(existing, dottedName, block) {
193
- const trimmed = existing.trimEnd();
194
- const pattern = new RegExp(`\\n?\\[${escapeRegExp(dottedName)}\\][\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`);
195
- if (pattern.test(trimmed)) {
196
- return `${trimmed.replace(pattern, `\n${block.trimEnd()}`)}\n`;
200
+ const trimmedBlock = block.trimEnd();
201
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
202
+ const target = normalizeTomlDottedName(dottedName);
203
+ const start = lines.findIndex((line) => normalizeTomlDottedName(parseTomlTableHeader(line)) === target);
204
+
205
+ if (start !== -1) {
206
+ let end = lines.length;
207
+ for (let index = start + 1; index < lines.length; index += 1) {
208
+ const tableName = normalizeTomlDottedName(parseTomlTableHeader(lines[index]));
209
+ if (!tableName) {
210
+ continue;
211
+ }
212
+ if (tableName === target || tableName.startsWith(`${target}.`)) {
213
+ continue;
214
+ }
215
+ end = index;
216
+ break;
217
+ }
218
+
219
+ const prefix = lines.slice(0, start).join("\n").trimEnd();
220
+ const suffix = lines.slice(end).join("\n").trimStart();
221
+ return [
222
+ prefix,
223
+ trimmedBlock,
224
+ suffix
225
+ ].filter(Boolean).join("\n").trimEnd() + "\n";
197
226
  }
227
+
228
+ const trimmed = existing.trimEnd();
198
229
  return `${trimmed}${trimmed ? "\n\n" : ""}${block}`;
199
230
  }
200
231
 
@@ -216,20 +247,25 @@ async function writeInstallFile({ agent, path: targetPath, dryRun, content }) {
216
247
  };
217
248
  }
218
249
 
219
- async function readJsonFile(filePath, fallback) {
250
+ async function readJsonConfigFile(filePath) {
220
251
  if (!existsSync(filePath)) {
221
- return fallback;
252
+ return {};
222
253
  }
223
254
 
224
255
  const raw = await readTextFile(filePath, "");
225
256
  if (!raw.trim()) {
226
- return fallback;
257
+ return {};
227
258
  }
259
+ let parsed;
228
260
  try {
229
- return JSON.parse(raw);
261
+ parsed = JSON.parse(raw);
230
262
  } catch (error) {
231
263
  throw new Error(`Failed to parse ${filePath}: ${error.message}`);
232
264
  }
265
+ if (!isPlainObject(parsed)) {
266
+ throw new Error(`Invalid MCP config at ${filePath}: expected a JSON object`);
267
+ }
268
+ return parsed;
233
269
  }
234
270
 
235
271
  async function readTextFile(filePath, fallback) {
@@ -247,6 +283,20 @@ function normalizeAgent(agent) {
247
283
  return normalized;
248
284
  }
249
285
 
286
+ function jsonObjectSection(config, key, filePath) {
287
+ if (config[key] === undefined) {
288
+ return {};
289
+ }
290
+ if (!isPlainObject(config[key])) {
291
+ throw new Error(`Invalid MCP config at ${filePath}: ${key} must be a JSON object`);
292
+ }
293
+ return config[key];
294
+ }
295
+
296
+ function isPlainObject(value) {
297
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
298
+ }
299
+
250
300
  function normalizeTimeoutMs(value) {
251
301
  const timeout = Number(value ?? DEFAULT_MCP_TIMEOUT_MS);
252
302
  return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_MCP_TIMEOUT_MS;
@@ -276,6 +326,47 @@ function quoteTomlString(value) {
276
326
  return JSON.stringify(String(value));
277
327
  }
278
328
 
279
- function escapeRegExp(value) {
280
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
329
+ function parseTomlTableHeader(line) {
330
+ const match = /^\s*\[([^\][\r\n]+)]\s*(?:#.*)?$/.exec(line);
331
+ return match ? match[1] : "";
332
+ }
333
+
334
+ function normalizeTomlDottedName(value) {
335
+ return splitTomlDottedName(value).join(".");
336
+ }
337
+
338
+ function splitTomlDottedName(value) {
339
+ const parts = [];
340
+ let current = "";
341
+ let quoted = false;
342
+ let quote = "";
343
+ let escaped = false;
344
+ for (const char of String(value || "").trim()) {
345
+ if (quoted) {
346
+ if (escaped) {
347
+ current += char;
348
+ escaped = false;
349
+ } else if (char === "\\" && quote === "\"") {
350
+ escaped = true;
351
+ } else if (char === quote) {
352
+ quoted = false;
353
+ } else {
354
+ current += char;
355
+ }
356
+ continue;
357
+ }
358
+ if (char === "\"" || char === "'") {
359
+ quoted = true;
360
+ quote = char;
361
+ } else if (char === ".") {
362
+ parts.push(current.trim());
363
+ current = "";
364
+ } else if (!/\s/.test(char)) {
365
+ current += char;
366
+ }
367
+ }
368
+ if (current || parts.length > 0) {
369
+ parts.push(current.trim());
370
+ }
371
+ return parts.filter(Boolean);
281
372
  }