artifacty 0.1.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/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "artifacty",
3
+ "version": "0.1.0",
4
+ "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "llm",
8
+ "agent",
9
+ "artifacts",
10
+ "mcp",
11
+ "claude",
12
+ "codex",
13
+ "gemini"
14
+ ],
15
+ "homepage": "https://github.com/raeseoklee/artifacty#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/raeseoklee/artifacty/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/raeseoklee/artifacty.git"
22
+ },
23
+ "bin": {
24
+ "artifacty": "src/cli.js",
25
+ "artifacty-mcp": "src/mcp-server.js"
26
+ },
27
+ "scripts": {
28
+ "start": "node src/server.js",
29
+ "mcp": "node src/mcp-server.js",
30
+ "test": "node --test",
31
+ "lint": "node --check src/*.js src/lib/*.js src/client/*.js test/*.test.js",
32
+ "smoke": "bash scripts/smoke.sh",
33
+ "release:check": "npm run lint && npm test && npm run smoke"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "docs",
38
+ "scripts/smoke.sh",
39
+ "README.md",
40
+ "LICENSE",
41
+ "THIRD_PARTY_NOTICES.md",
42
+ "AGENTS.md",
43
+ "CLAUDE.md"
44
+ ],
45
+ "engines": {
46
+ "node": ">=22.5"
47
+ },
48
+ "license": "MIT",
49
+ "dependencies": {
50
+ "@codemirror/lang-html": "^6.4.11",
51
+ "@codemirror/lang-json": "^6.0.2",
52
+ "@codemirror/lang-markdown": "^6.5.0",
53
+ "codemirror": "^6.0.2"
54
+ }
55
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ HOME_DIR="$(mktemp -d "${TMPDIR:-/tmp}/artifacty-smoke-XXXXXX")"
6
+ PORT="${ARTIFACTY_SMOKE_PORT:-$((18000 + RANDOM % 1000))}"
7
+ URL="http://127.0.0.1:${PORT}"
8
+ TOKEN="artifacty-smoke-token"
9
+ SERVER_PID=""
10
+
11
+ cleanup() {
12
+ if [[ -n "${SERVER_PID}" ]]; then
13
+ kill "${SERVER_PID}" 2>/dev/null || true
14
+ wait "${SERVER_PID}" 2>/dev/null || true
15
+ fi
16
+ rm -rf "${HOME_DIR}"
17
+ }
18
+ trap cleanup EXIT
19
+
20
+ ARTIFACTY_HOME="${HOME_DIR}" ARTIFACTY_API_TOKEN="${TOKEN}" \
21
+ node "${ROOT}/src/server.js" --host 127.0.0.1 --port "${PORT}" \
22
+ >"${HOME_DIR}/server.out.log" 2>"${HOME_DIR}/server.err.log" &
23
+ SERVER_PID="$!"
24
+
25
+ node --input-type=module - "${URL}" "${TOKEN}" <<'NODE'
26
+ const [url, token] = process.argv.slice(2);
27
+
28
+ async function waitForHealth() {
29
+ const deadline = Date.now() + 5000;
30
+ while (Date.now() < deadline) {
31
+ try {
32
+ const response = await fetch(`${url}/health`);
33
+ if (response.ok) return;
34
+ } catch {}
35
+ await new Promise((resolve) => setTimeout(resolve, 100));
36
+ }
37
+ throw new Error("server did not become healthy");
38
+ }
39
+
40
+ function assert(condition, message) {
41
+ if (!condition) throw new Error(message);
42
+ }
43
+
44
+ await waitForHealth();
45
+
46
+ let editorResponse = await fetch(`${url}/new`);
47
+ assert(editorResponse.status === 200, `expected editor page status 200, got ${editorResponse.status}`);
48
+ let editorPage = await editorResponse.text();
49
+ assert(editorPage.includes("/assets/editor.js"), "editor page missing CodeMirror script");
50
+
51
+ editorResponse = await fetch(`${url}/assets/editor.js`);
52
+ assert(editorResponse.status === 200, `expected editor asset status 200, got ${editorResponse.status}`);
53
+
54
+ editorResponse = await fetch(`${url}/vendor/npm/codemirror`);
55
+ assert(editorResponse.status === 200, `expected CodeMirror vendor status 200, got ${editorResponse.status}`);
56
+
57
+ let response = await fetch(`${url}/api/artifacts`);
58
+ assert(response.status === 401, `expected unauthenticated API to return 401, got ${response.status}`);
59
+
60
+ response = await fetch(`${url}/api/artifacts`, {
61
+ method: "POST",
62
+ headers: {
63
+ "content-type": "application/json",
64
+ "x-artifacty-token": token
65
+ },
66
+ body: JSON.stringify({
67
+ title: "Smoke Artifact",
68
+ content: "# Smoke",
69
+ format: "markdown",
70
+ sourceAgent: "smoke"
71
+ })
72
+ });
73
+ assert(response.status === 201, `expected create status 201, got ${response.status}`);
74
+ const created = await response.json();
75
+ assert(created.id && created.rawUrl, "create response missing artifact URLs");
76
+
77
+ response = await fetch(`${url}/api/artifacts`, {
78
+ method: "POST",
79
+ headers: {
80
+ "content-type": "application/json",
81
+ "x-artifacty-token": token
82
+ },
83
+ body: JSON.stringify({
84
+ title: "Blocked Secret",
85
+ content: "ghp_abcdefghijklmnopqrstuvwxyz123456",
86
+ format: "text"
87
+ })
88
+ });
89
+ assert(response.status === 400, `expected secret scan status 400, got ${response.status}`);
90
+ const blocked = await response.json();
91
+ assert(blocked.code === "SECRET_DETECTED", `unexpected secret scan code ${blocked.code}`);
92
+
93
+ response = await fetch(`${url}/api/audit`, {
94
+ headers: { "x-artifacty-token": token }
95
+ });
96
+ assert(response.status === 200, `expected audit status 200, got ${response.status}`);
97
+ const audit = await response.json();
98
+ assert(audit.events.some((event) => event.action === "create"), "audit log missing create event");
99
+ NODE
100
+
101
+ ARTIFACTY_HOME="${HOME_DIR}" node "${ROOT}/src/cli.js" backup --file "${HOME_DIR}/backup.json" >/dev/null
102
+ ARTIFACTY_HOME="${HOME_DIR}" node "${ROOT}/src/cli.js" check --home "${HOME_DIR}" >/dev/null
103
+
104
+ printf 'Artifacty smoke passed: %s\n' "${URL}"
package/src/cli.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ archiveArtifact,
7
+ createArtifact,
8
+ createStore,
9
+ getArtifact,
10
+ listAuditEvents,
11
+ listArtifacts,
12
+ restoreArtifact,
13
+ updateArtifact
14
+ } from "./lib/storage.js";
15
+ import { exportStore, importStore, defaultBackupPath } from "./lib/backup.js";
16
+ import { convertAgentArtifact } from "./lib/converters.js";
17
+ import { checkMcpTools } from "./lib/check.js";
18
+ import { installAgent } from "./lib/installer.js";
19
+ import { serviceCommand } from "./lib/service.js";
20
+ import { resolvePublicBaseUrl } from "./lib/server-state.js";
21
+ import { startServer } from "./server.js";
22
+
23
+ const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
24
+
25
+ async function main() {
26
+ const [command, ...args] = process.argv.slice(2);
27
+ const options = parseArgs(args);
28
+ const store = createStore({ home: options.home });
29
+
30
+ if (!command || command === "help" || command === "--help" || command === "-h") {
31
+ printHelp();
32
+ return;
33
+ }
34
+
35
+ if (command === "serve") {
36
+ const server = await startServer({
37
+ host: options.host,
38
+ port: options.port,
39
+ home: options.home,
40
+ apiToken: options.apiToken,
41
+ shareMode: options.shareMode,
42
+ allowSecrets: options.allowSecrets
43
+ });
44
+ process.stderr.write(`Artifacty listening on ${server.url}\n`);
45
+ process.stderr.write(`Store: ${server.store.home}\n`);
46
+ return;
47
+ }
48
+
49
+ if (command === "publish") {
50
+ const content = await readContent(options);
51
+ const artifact = await createArtifact(store, {
52
+ title: requireOption(options, "title"),
53
+ content,
54
+ format: options.format,
55
+ artifactType: options.artifactType,
56
+ schemaVersion: options.schemaVersion,
57
+ sourceAgent: options.source || "cli",
58
+ tags: options.tag || [],
59
+ metadata: options.metadata ? JSON.parse(options.metadata) : {},
60
+ allowSecrets: options.allowSecrets,
61
+ audit: cliAuditContext()
62
+ });
63
+ printJson(await withUrls(store, artifact));
64
+ return;
65
+ }
66
+
67
+ if (command === "import") {
68
+ const content = await readContent(options);
69
+ const sourcePath = options.file ? path.resolve(options.file) : "";
70
+ const converted = convertAgentArtifact({
71
+ agent: options.agent || options.source || "auto",
72
+ title: options.title,
73
+ content,
74
+ format: options.format,
75
+ artifactType: options.artifactType,
76
+ schemaVersion: options.schemaVersion,
77
+ contentType: options.contentType,
78
+ fileName: options.file ? path.basename(options.file) : options.fileName,
79
+ sourcePath,
80
+ sourceAgent: options.sourceAgent,
81
+ tags: options.tag || [],
82
+ metadata: options.metadata ? JSON.parse(options.metadata) : {}
83
+ });
84
+ const artifact = await createArtifact(store, {
85
+ ...converted,
86
+ allowSecrets: options.allowSecrets,
87
+ auditAction: "import",
88
+ audit: cliAuditContext()
89
+ });
90
+ printJson(await withUrls(store, artifact));
91
+ return;
92
+ }
93
+
94
+ if (command === "install") {
95
+ const agent = options._[0];
96
+ if (!agent) {
97
+ throw new Error("install requires an agent: claude, codex, gemini, or all");
98
+ }
99
+ const result = await installAgent(agent, {
100
+ projectDir: options.projectDir || process.cwd(),
101
+ packageDir: PACKAGE_ROOT,
102
+ configPath: options.config,
103
+ serverPath: options.serverPath,
104
+ url: options.url,
105
+ home: options.home,
106
+ dryRun: options.dryRun,
107
+ trust: options.trust,
108
+ timeout: options.timeout
109
+ });
110
+ printJson(stripInstallContentUnlessDryRun(result));
111
+ return;
112
+ }
113
+
114
+ if (command === "check") {
115
+ const result = await checkMcpTools({
116
+ projectDir: options.projectDir || PACKAGE_ROOT,
117
+ serverPath: options.serverPath,
118
+ url: options.url,
119
+ home: options.home,
120
+ timeout: options.timeout
121
+ });
122
+ printJson(result);
123
+ if (!result.ok) {
124
+ process.exitCode = 1;
125
+ }
126
+ return;
127
+ }
128
+
129
+ if (command === "update") {
130
+ const id = args.find((arg) => !arg.startsWith("-"));
131
+ if (!id) {
132
+ throw new Error("update requires an artifact id");
133
+ }
134
+ const content = await readContent(options);
135
+ const artifact = await updateArtifact(store, id, {
136
+ title: options.title,
137
+ content,
138
+ format: options.format,
139
+ artifactType: options.artifactType,
140
+ schemaVersion: options.schemaVersion,
141
+ sourceAgent: options.source || "cli",
142
+ tags: options.tag || [],
143
+ metadata: options.metadata ? JSON.parse(options.metadata) : {},
144
+ allowSecrets: options.allowSecrets,
145
+ audit: cliAuditContext()
146
+ });
147
+ printJson(await withUrls(store, artifact));
148
+ return;
149
+ }
150
+
151
+ if (command === "list") {
152
+ const artifacts = await listArtifacts(store, {
153
+ query: options.query,
154
+ tag: Array.isArray(options.tag) ? options.tag[0] : options.tag,
155
+ sourceAgent: options.source,
156
+ includeArchived: options.includeArchived,
157
+ limit: options.limit
158
+ });
159
+ printJson({ artifacts });
160
+ return;
161
+ }
162
+
163
+ if (command === "archive" || command === "restore") {
164
+ const id = options._[0];
165
+ if (!id) {
166
+ throw new Error(`${command} requires an artifact id`);
167
+ }
168
+ const artifact = command === "archive"
169
+ ? await archiveArtifact(store, id, { audit: cliAuditContext() })
170
+ : await restoreArtifact(store, id, { audit: cliAuditContext() });
171
+ printJson(await withUrls(store, artifact));
172
+ return;
173
+ }
174
+
175
+ if (command === "audit") {
176
+ const events = await listAuditEvents(store, {
177
+ artifactId: options.artifact,
178
+ limit: options.limit
179
+ });
180
+ printJson({ events });
181
+ return;
182
+ }
183
+
184
+ if (command === "export" || command === "backup") {
185
+ const file = command === "backup" ? options.file || defaultBackupPath(store) : requireOption(options, "file");
186
+ printJson(await exportStore(store, file));
187
+ return;
188
+ }
189
+
190
+ if (command === "import-store") {
191
+ printJson(await importStore(store, requireOption(options, "file")));
192
+ return;
193
+ }
194
+
195
+ if (command === "service") {
196
+ const action = options._[0] || "plist";
197
+ printJson(await serviceCommand(action, {
198
+ projectDir: options.projectDir || PACKAGE_ROOT,
199
+ serverPath: options.serverPath,
200
+ plistPath: options.plist,
201
+ host: options.host,
202
+ port: options.port,
203
+ home: options.home,
204
+ dryRun: options.dryRun
205
+ }));
206
+ return;
207
+ }
208
+
209
+ if (command === "show") {
210
+ const id = args.find((arg) => !arg.startsWith("-"));
211
+ if (!id) {
212
+ throw new Error("show requires an artifact id");
213
+ }
214
+ const artifact = await getArtifact(store, id, { version: options.version });
215
+ if (options.raw) {
216
+ process.stdout.write(artifact.content);
217
+ return;
218
+ }
219
+ printJson(await withUrls(store, artifact));
220
+ return;
221
+ }
222
+
223
+ throw new Error(`Unknown command: ${command}`);
224
+ }
225
+
226
+ function parseArgs(args) {
227
+ const options = {};
228
+ const positional = [];
229
+
230
+ for (let index = 0; index < args.length; index += 1) {
231
+ const arg = args[index];
232
+ if (!arg.startsWith("--")) {
233
+ positional.push(arg);
234
+ continue;
235
+ }
236
+
237
+ const key = arg.slice(2);
238
+ if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets") {
239
+ options[toCamelCase(key)] = true;
240
+ continue;
241
+ }
242
+
243
+ const value = args[++index];
244
+ if (value === undefined) {
245
+ throw new Error(`Missing value for --${key}`);
246
+ }
247
+
248
+ if (key === "tag") {
249
+ options.tag = [...(options.tag || []), value];
250
+ } else if (key === "port" || key === "limit" || key === "version" || key === "schema-version" || key === "timeout") {
251
+ options[toCamelCase(key)] = Number(value);
252
+ } else {
253
+ options[toCamelCase(key)] = value;
254
+ }
255
+ }
256
+
257
+ options._ = positional;
258
+ return options;
259
+ }
260
+
261
+ async function readContent(options) {
262
+ if (options.file) {
263
+ return readFile(options.file, "utf8");
264
+ }
265
+ if (options.content !== undefined) {
266
+ return options.content;
267
+ }
268
+ throw new Error("Provide --file or --content");
269
+ }
270
+
271
+ async function withUrls(store, artifact) {
272
+ const publicBaseUrl = await resolvePublicBaseUrl(store);
273
+ return {
274
+ ...artifact,
275
+ url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`,
276
+ rawUrl: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${artifact.version.version}`
277
+ };
278
+ }
279
+
280
+ function requireOption(options, name) {
281
+ if (!options[name]) {
282
+ throw new Error(`Missing required option --${name}`);
283
+ }
284
+ return options[name];
285
+ }
286
+
287
+ function printJson(data) {
288
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
289
+ }
290
+
291
+ function printHelp() {
292
+ process.stdout.write(`Artifacty
293
+
294
+ Usage:
295
+ artifacty serve [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty]
296
+ artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json] [--source agent] [--tag tag]
297
+ artifacty import --agent claude|codex|gemini|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json] [--tag tag]
298
+ artifacty install claude|codex|gemini|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787]
299
+ artifacty check [--server-path <path>] [--timeout 5000]
300
+ artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json]
301
+ artifacty archive <id>
302
+ artifacty restore <id>
303
+ artifacty audit [--artifact <id>] [--limit 100]
304
+ artifacty export --file <path>
305
+ artifacty backup [--file <path>]
306
+ artifacty import-store --file <path>
307
+ artifacty service plist|install|uninstall [--dry-run] [--plist <path>]
308
+ artifacty list [--query text] [--tag tag] [--source agent] [--limit 50] [--include-archived]
309
+ artifacty show <id> [--version n] [--raw]
310
+
311
+ Environment:
312
+ ARTIFACTY_HOME Storage directory. Defaults to ~/.artifacty
313
+ ARTIFACTY_URL Public URL override. Otherwise CLI/MCP read the last running server URL
314
+ ARTIFACTY_API_TOKEN Required token for HTTP API and LAN mode
315
+ ARTIFACTY_SHARE_MODE Use lan or team before binding outside localhost
316
+ ARTIFACTY_ALLOW_SECRETS Set true only to intentionally store detected secrets
317
+ `);
318
+ }
319
+
320
+ function stripInstallContentUnlessDryRun(result) {
321
+ if (result.results) {
322
+ return {
323
+ ...result,
324
+ results: result.results.map(stripInstallContentUnlessDryRun)
325
+ };
326
+ }
327
+ if (result.dryRun) {
328
+ return result;
329
+ }
330
+ const { content, ...rest } = result;
331
+ return rest;
332
+ }
333
+
334
+ function toCamelCase(value) {
335
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
336
+ }
337
+
338
+ function cliAuditContext() {
339
+ return {
340
+ surface: "cli",
341
+ actor: process.env.USER || process.env.LOGNAME || "cli"
342
+ };
343
+ }
344
+
345
+ main().catch((error) => {
346
+ process.stderr.write(`${error.message}\n`);
347
+ process.exitCode = 1;
348
+ });