artifacty 0.7.0 → 0.8.0

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 CHANGED
@@ -82,6 +82,13 @@ artifacty install all
82
82
  artifacty check
83
83
  ```
84
84
 
85
+ Run diagnostics for the local runtime, store, server, service definitions, and MCP discovery:
86
+
87
+ ```bash
88
+ artifacty doctor
89
+ artifacty doctor --skip-mcp
90
+ ```
91
+
85
92
  Use `artifacty install codex --timeout 30000` or
86
93
  `artifacty install gemini --timeout 30000` to tune supported MCP client timeouts.
87
94
 
@@ -190,6 +197,7 @@ Operational commands:
190
197
 
191
198
  ```bash
192
199
  artifacty audit --limit 20
200
+ artifacty doctor
193
201
  artifacty index rebuild
194
202
  artifacty integrity
195
203
  artifacty backup
@@ -37,6 +37,15 @@ The lifecycle commands are intended to be cross-platform:
37
37
  - Windows: `start` hides the child console window, and `stop` uses `taskkill /PID <pid> /T`, falling back to `/F` when Windows requires forceful termination. `--force` uses `/F` immediately.
38
38
  - All platforms: `status` combines the managed pid file with the HTTP `/health` endpoint, so a stale pid alone is not reported as healthy.
39
39
 
40
+ Run diagnostics when setup behaves unexpectedly:
41
+
42
+ ```bash
43
+ node src/cli.js doctor
44
+ node src/cli.js doctor --skip-mcp
45
+ ```
46
+
47
+ `doctor` checks Node version support, local exposure settings, store integrity, managed server health, service definition rendering, and MCP discovery. A stopped server is reported as a warning, not a failure.
48
+
40
49
  For login/startup persistence, use the operating system's service manager. Artifacty's `service` command can generate macOS LaunchAgent, Linux systemd user-unit, and Windows Task Scheduler definitions.
41
50
 
42
51
  Create artifacts directly in the browser at `http://127.0.0.1:8787/new`.
@@ -85,6 +94,7 @@ node src/cli.js check
85
94
  - `--dry-run` returns the generated config without writing it.
86
95
  - `--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.
87
96
  - `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
97
+ - `doctor` combines MCP discovery with runtime, storage, server, and service diagnostics.
88
98
 
89
99
  ## Client Compatibility Matrix
90
100
 
@@ -10,6 +10,12 @@ npm run release:check
10
10
 
11
11
  This runs syntax checks, the full Node test suite, and a local smoke test that starts the HTTP server with token auth enabled, creates an artifact, verifies secret blocking, reads audit logs, writes a backup, and checks MCP tool/resource/prompt discovery.
12
12
 
13
+ Run the local diagnostics pass:
14
+
15
+ ```bash
16
+ artifacty doctor
17
+ ```
18
+
13
19
  ## Packaging
14
20
 
15
21
  - Confirm `package.json` `version` and `files` are intentional.
@@ -47,4 +53,5 @@ This runs syntax checks, the full Node test suite, and a local smoke test that s
47
53
 
48
54
  - Export a backup before upgrades: `artifacty backup`.
49
55
  - Confirm `artifacty audit --limit 20` shows recent create/update/read/archive events.
56
+ - Confirm `artifacty doctor` reports no failures. A stopped server warning is acceptable when intentionally checking an offline store.
50
57
  - For background service installs, dry-run first: `artifacty service install --dry-run`. Use `--platform macos|linux|windows` to review another OS definition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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
@@ -22,6 +22,7 @@ import { serviceCommand } from "./lib/service.js";
22
22
  import { backgroundStatus, startBackgroundServer, stopBackgroundServer } from "./lib/background.js";
23
23
  import { resolvePublicBaseUrl } from "./lib/server-state.js";
24
24
  import { generateToken } from "./lib/token.js";
25
+ import { runDoctor } from "./lib/doctor.js";
25
26
  import { startServer } from "./server.js";
26
27
 
27
28
  const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
@@ -105,6 +106,27 @@ async function main() {
105
106
  return;
106
107
  }
107
108
 
109
+ if (command === "doctor") {
110
+ const result = await runDoctor({
111
+ packageRoot: PACKAGE_ROOT,
112
+ serverPath: options.serverPath,
113
+ url: options.url,
114
+ home: options.home,
115
+ host: options.host,
116
+ port: options.port,
117
+ apiToken: options.apiToken,
118
+ shareMode: options.shareMode,
119
+ allowSecrets: options.allowSecrets,
120
+ timeout: options.timeout,
121
+ skipMcp: options.skipMcp
122
+ });
123
+ printJson(result);
124
+ if (!result.ok) {
125
+ process.exitCode = 1;
126
+ }
127
+ return;
128
+ }
129
+
108
130
  if (command === "publish") {
109
131
  const content = await readContent(options);
110
132
  const artifact = await createArtifact(store, {
@@ -324,7 +346,7 @@ function parseArgs(args) {
324
346
  }
325
347
 
326
348
  const key = arg.slice(2);
327
- if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force") {
349
+ if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force" || key === "skip-mcp") {
328
350
  options[toCamelCase(key)] = true;
329
351
  continue;
330
352
  }
@@ -412,6 +434,7 @@ Usage:
412
434
  artifacty start [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--generate-token] [--timeout 30000]
413
435
  artifacty status [--home ~/.artifacty]
414
436
  artifacty stop [--home ~/.artifacty] [--timeout 30000] [--force]
437
+ artifacty doctor [--home ~/.artifacty] [--skip-mcp] [--timeout 5000]
415
438
  artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react] [--source agent] [--tag tag]
416
439
  artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react] [--tag tag]
417
440
  artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--timeout 30000]
@@ -0,0 +1,214 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { arch, platform } from "node:os";
4
+ import { backgroundStatus } from "./background.js";
5
+ import { checkMcpTools } from "./check.js";
6
+ import { securityConfig, validateServerExposure, exposureWarning } from "./security.js";
7
+ import { serviceCommand } from "./service.js";
8
+ import { checkStoreIntegrity, createStore } from "./storage.js";
9
+
10
+ const MIN_NODE_VERSION = "22.5.0";
11
+
12
+ export async function runDoctor(options = {}) {
13
+ const packageRoot = path.resolve(options.packageRoot || process.cwd());
14
+ const store = createStore({ home: options.home });
15
+ const checks = [];
16
+
17
+ await collect(checks, "runtime", () => runtimeCheck(packageRoot));
18
+ await collect(checks, "security", () => securityCheck(options));
19
+ await collect(checks, "storage", () => storageCheck(store));
20
+ await collect(checks, "server", () => serverCheck(store));
21
+ await collect(checks, "service", () => serviceCheck(options));
22
+ if (options.skipMcp) {
23
+ checks.push({
24
+ name: "mcp",
25
+ status: "skip",
26
+ message: "MCP discovery skipped by --skip-mcp"
27
+ });
28
+ } else {
29
+ await collect(checks, "mcp", () => mcpCheck(packageRoot, options));
30
+ }
31
+
32
+ const failures = checks.filter((check) => check.status === "fail");
33
+ const warnings = checks.filter((check) => check.status === "warn");
34
+
35
+ return {
36
+ ok: failures.length === 0,
37
+ checkedAt: new Date().toISOString(),
38
+ version: await packageVersion(packageRoot),
39
+ home: store.home,
40
+ platform: process.platform,
41
+ checks,
42
+ failures: failures.map(({ name, message }) => ({ name, message })),
43
+ warnings: warnings.map(({ name, message }) => ({ name, message }))
44
+ };
45
+ }
46
+
47
+ async function collect(checks, name, fn) {
48
+ try {
49
+ checks.push({ name, ...(await fn()) });
50
+ } catch (error) {
51
+ checks.push({
52
+ name,
53
+ status: "fail",
54
+ message: error.message,
55
+ error: {
56
+ name: error.name,
57
+ code: error.code
58
+ }
59
+ });
60
+ }
61
+ }
62
+
63
+ async function runtimeCheck(packageRoot) {
64
+ const version = await packageVersion(packageRoot);
65
+ const supported = compareVersions(process.versions.node, MIN_NODE_VERSION) >= 0;
66
+ return {
67
+ status: supported ? "pass" : "fail",
68
+ message: supported
69
+ ? `Node ${process.versions.node} satisfies >=${MIN_NODE_VERSION}`
70
+ : `Node ${process.versions.node} is below required >=${MIN_NODE_VERSION}`,
71
+ data: {
72
+ artifactyVersion: version,
73
+ nodeVersion: process.versions.node,
74
+ requiredNodeVersion: `>=${MIN_NODE_VERSION}`,
75
+ platform: platform(),
76
+ arch: arch()
77
+ }
78
+ };
79
+ }
80
+
81
+ function securityCheck(options = {}) {
82
+ const host = options.host || process.env.ARTIFACTY_HOST || "127.0.0.1";
83
+ const config = securityConfig(options);
84
+ validateServerExposure({ host, config });
85
+ const warning = exposureWarning({ host, config });
86
+ if (warning) {
87
+ return {
88
+ status: "warn",
89
+ message: warning,
90
+ data: {
91
+ host,
92
+ shareMode: config.shareMode,
93
+ hasApiToken: Boolean(config.apiToken)
94
+ }
95
+ };
96
+ }
97
+ return {
98
+ status: "pass",
99
+ message: "Server exposure settings are local-first",
100
+ data: {
101
+ host,
102
+ shareMode: config.shareMode,
103
+ hasApiToken: Boolean(config.apiToken)
104
+ }
105
+ };
106
+ }
107
+
108
+ async function storageCheck(store) {
109
+ const integrity = await checkStoreIntegrity(store);
110
+ return {
111
+ status: integrity.ok ? "pass" : "fail",
112
+ message: integrity.ok
113
+ ? `Store is consistent with ${integrity.artifactCount} artifacts and ${integrity.versionCount} versions`
114
+ : "Store integrity check found missing, changed, orphaned, or inconsistent files",
115
+ data: integrity
116
+ };
117
+ }
118
+
119
+ async function serverCheck(store) {
120
+ const status = await backgroundStatus({ home: store.home });
121
+ if (status.running) {
122
+ return {
123
+ status: "pass",
124
+ message: `Managed server is healthy at ${status.url}`,
125
+ data: status
126
+ };
127
+ }
128
+ if (status.processRunning || status.pidFileExists) {
129
+ return {
130
+ status: "fail",
131
+ message: "Recorded server process or pid file exists but health check is not passing",
132
+ data: status
133
+ };
134
+ }
135
+ return {
136
+ status: "warn",
137
+ message: "No managed Artifacty server is currently running",
138
+ data: status
139
+ };
140
+ }
141
+
142
+ async function serviceCheck(options = {}) {
143
+ const definitions = [];
144
+ for (const action of ["plist", "unit", "task"]) {
145
+ const result = await serviceCommand(action, {
146
+ projectDir: options.packageRoot || process.cwd(),
147
+ serverPath: options.serverPath,
148
+ host: options.host,
149
+ port: options.port,
150
+ home: options.home,
151
+ apiToken: options.apiToken,
152
+ shareMode: options.shareMode,
153
+ allowSecrets: options.allowSecrets,
154
+ dryRun: true
155
+ });
156
+ definitions.push({
157
+ action,
158
+ platform: result.platform,
159
+ path: result.path,
160
+ contentBytes: Buffer.byteLength(result.content || "", "utf8")
161
+ });
162
+ }
163
+ return {
164
+ status: "pass",
165
+ message: "Service definitions render for macOS, Linux, and Windows",
166
+ data: { definitions }
167
+ };
168
+ }
169
+
170
+ async function mcpCheck(packageRoot, options = {}) {
171
+ const result = await checkMcpTools({
172
+ projectDir: packageRoot,
173
+ serverPath: options.serverPath,
174
+ url: options.url,
175
+ home: options.home,
176
+ timeout: options.timeout
177
+ });
178
+ return {
179
+ status: result.ok ? "pass" : "fail",
180
+ message: result.ok
181
+ ? `MCP discovery found ${result.toolCount} tools, ${result.resourceCount} resources, and ${result.promptCount} prompts`
182
+ : "MCP discovery is missing required tools, resources, or prompts",
183
+ data: result
184
+ };
185
+ }
186
+
187
+ async function packageVersion(packageRoot) {
188
+ const file = path.join(packageRoot, "package.json");
189
+ const parsed = JSON.parse(await readFile(file, "utf8"));
190
+ return parsed.version;
191
+ }
192
+
193
+ function compareVersions(left, right) {
194
+ const leftParts = versionParts(left);
195
+ const rightParts = versionParts(right);
196
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
197
+ const leftPart = leftParts[index] || 0;
198
+ const rightPart = rightParts[index] || 0;
199
+ if (leftPart > rightPart) {
200
+ return 1;
201
+ }
202
+ if (leftPart < rightPart) {
203
+ return -1;
204
+ }
205
+ }
206
+ return 0;
207
+ }
208
+
209
+ function versionParts(value) {
210
+ return String(value)
211
+ .split(".")
212
+ .map((part) => Number.parseInt(part, 10))
213
+ .filter((part) => Number.isFinite(part));
214
+ }