@artifactly/mcp 0.1.6 → 0.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artifactly/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -57,9 +57,32 @@ export function getDefaultCredentialPath(home = homedir()) {
57
57
  return join(home, ".artifactly", "credentials.json");
58
58
  }
59
59
 
60
+ function normalizeCredentials(value: unknown): ArtifactlyCredentials | null {
61
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
62
+ return null;
63
+ }
64
+ const candidate = value as Record<string, unknown>;
65
+ if (typeof candidate.apiBaseUrl !== "string" || !candidate.apiBaseUrl.trim()) {
66
+ return null;
67
+ }
68
+ if (typeof candidate.apiKey !== "string" || !candidate.apiKey.trim()) {
69
+ return null;
70
+ }
71
+ return {
72
+ apiBaseUrl: candidate.apiBaseUrl.trim(),
73
+ apiKey: candidate.apiKey.trim(),
74
+ ...(typeof candidate.email === "string" && candidate.email.trim()
75
+ ? { email: candidate.email.trim() }
76
+ : {}),
77
+ ...(typeof candidate.workspace === "string" && candidate.workspace.trim()
78
+ ? { workspace: candidate.workspace.trim() }
79
+ : {}),
80
+ };
81
+ }
82
+
60
83
  export function readCredentials(path = getDefaultCredentialPath()): ArtifactlyCredentials | null {
61
84
  try {
62
- return JSON.parse(readFileSync(path, "utf8")) as ArtifactlyCredentials;
85
+ return normalizeCredentials(JSON.parse(readFileSync(path, "utf8")));
63
86
  } catch {
64
87
  return null;
65
88
  }
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import packageJson from "../../package.json";
4
4
  import {
5
+ createArtifactlyMcpServer,
5
6
  createArtifactlyMcpCallResult,
6
7
  getArtifactlyMcpInstallConfig,
7
8
  listArtifactlyMcpServerTools,
@@ -37,6 +38,25 @@ describe("Artifactly MCP server packaging", () => {
37
38
  });
38
39
 
39
40
  describe("Artifactly MCP stdio server helpers", () => {
41
+ it("registers tool schemas and server info from package metadata", () => {
42
+ const server = createArtifactlyMcpServer() as unknown as {
43
+ server: { _serverInfo: { name: string; version: string } };
44
+ _registeredTools: Record<string, { inputSchema: { def: { shape: Record<string, unknown> }; safeParse(value: unknown): { success: boolean } } }>;
45
+ };
46
+
47
+ expect(server.server._serverInfo).toEqual({
48
+ name: "artifactly",
49
+ version: packageJson.version,
50
+ });
51
+ expect(Object.keys(server._registeredTools.artifactly_auth_status.inputSchema.def.shape)).toContain("validate");
52
+ expect(
53
+ server._registeredTools.artifactly_update_artifact.inputSchema.safeParse({
54
+ artifactId: "artifact_1",
55
+ description: "",
56
+ }).success,
57
+ ).toBe(true);
58
+ });
59
+
40
60
  it("maps Artifactly tools to MCP-compatible tool definitions", () => {
41
61
  expect(listArtifactlyMcpServerTools().map((tool) => tool.name)).toEqual([
42
62
  "artifactly_auth_status",
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
4
 
5
+ import packageJson from "../../package.json";
5
6
  import { callArtifactlyMcpTool, listArtifactlyMcpTools } from "./artifactly-mcp.js";
6
7
 
7
8
  export type ArtifactlyMcpInstallConfig = {
@@ -83,7 +84,9 @@ export async function createArtifactlyMcpCallResult(input: {
83
84
  }
84
85
  }
85
86
 
86
- const authStatusSchema = z.object({});
87
+ const authStatusSchema = z.object({
88
+ validate: z.boolean().optional(),
89
+ });
87
90
  const requestLoginCodeSchema = z.object({
88
91
  email: z.string().email(),
89
92
  name: z.string().optional(),
@@ -107,7 +110,7 @@ const updateArtifactSchema = z.object({
107
110
  title: z.string().min(1).optional(),
108
111
  markdown: z.string().min(1).optional(),
109
112
  content: z.string().min(1).optional(),
110
- description: z.string().min(1).optional(),
113
+ description: z.string().max(500).optional(),
111
114
  visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
112
115
  accessList: z.array(z.string().email()).optional(),
113
116
  });
@@ -137,7 +140,7 @@ const toolSchemas = {
137
140
  export function createArtifactlyMcpServer() {
138
141
  const server = new McpServer({
139
142
  name: "artifactly",
140
- version: "0.1.1",
143
+ version: packageJson.version,
141
144
  });
142
145
 
143
146
  for (const tool of listArtifactlyMcpTools()) {
@@ -48,6 +48,63 @@ describe("Artifactly MCP tools", () => {
48
48
  rmSync(dir, { recursive: true, force: true });
49
49
  });
50
50
 
51
+ it("does not report malformed credential files as authenticated", async () => {
52
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
53
+ const credentialPath = join(dir, "credentials.json");
54
+ writeFileSync(credentialPath, "{}\n");
55
+
56
+ await expect(
57
+ callArtifactlyMcpTool({ name: "artifactly_auth_status", credentialPath }),
58
+ ).resolves.toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
59
+ await expect(
60
+ callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath }),
61
+ ).rejects.toThrow(/artifactly_request_login_code[\s\S]*artifactly_verify_login_code/);
62
+
63
+ rmSync(dir, { recursive: true, force: true });
64
+ });
65
+
66
+ it("can validate auth status against the Artifactly API", async () => {
67
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
68
+ const credentialPath = join(dir, "credentials.json");
69
+ const requests: Array<{ method: string; path: string; apiKey?: string }> = [];
70
+ writeCredentials(
71
+ {
72
+ apiBaseUrl: "https://artifactly.example",
73
+ apiKey: "art_test_secret",
74
+ email: "agent@example.com",
75
+ workspace: "agent",
76
+ },
77
+ credentialPath,
78
+ );
79
+
80
+ await expect(
81
+ callArtifactlyMcpTool({
82
+ name: "artifactly_auth_status",
83
+ credentialPath,
84
+ arguments: { validate: true },
85
+ requestJson: async (method, path, _body, options) => {
86
+ requests.push({ method, path, apiKey: options?.apiKey });
87
+ return { artifacts: [] };
88
+ },
89
+ }),
90
+ ).resolves.toMatchObject({
91
+ authenticated: true,
92
+ serverAuthenticated: true,
93
+ validationError: null,
94
+ });
95
+ expect(requests).toEqual([
96
+ { method: "GET", path: "/api/artifacts", apiKey: "art_test_secret" },
97
+ ]);
98
+
99
+ rmSync(dir, { recursive: true, force: true });
100
+ });
101
+
102
+ it("gives complete login guidance when protected tools lack credentials", async () => {
103
+ await expect(
104
+ callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath: "/tmp/nope.json" }),
105
+ ).rejects.toThrow(/artifactly_request_login_code[\s\S]*artifactly_verify_login_code/);
106
+ });
107
+
51
108
  it("logs out by clearing the shared credential store", async () => {
52
109
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
53
110
  const credentialPath = join(dir, "credentials.json");
@@ -117,6 +174,34 @@ describe("Artifactly MCP tools", () => {
117
174
  rmSync(dir, { recursive: true, force: true });
118
175
  });
119
176
 
177
+ it("preserves markdown body whitespace when publishing inline markdown", async () => {
178
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
179
+ const credentialPath = join(dir, "credentials.json");
180
+ let publishedBody: unknown;
181
+ writeCredentials(
182
+ {
183
+ apiBaseUrl: "https://artifactly.example",
184
+ apiKey: "art_t...et",
185
+ email: "agent@example.com",
186
+ workspace: "agent",
187
+ },
188
+ credentialPath,
189
+ );
190
+
191
+ await callArtifactlyMcpTool({
192
+ name: "artifactly_publish_markdown",
193
+ credentialPath,
194
+ arguments: { title: "Whitespace", markdown: " # Hello\n\n" },
195
+ postJson: async (_path, body) => {
196
+ publishedBody = body;
197
+ return { id: "artifact_1", url: "https://artifactly.example/share/agent/hello" };
198
+ },
199
+ });
200
+
201
+ expect(publishedBody).toMatchObject({ content: " # Hello\n\n" });
202
+ rmSync(dir, { recursive: true, force: true });
203
+ });
204
+
120
205
  it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
121
206
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
122
207
  const credentialPath = join(dir, "credentials.json");
@@ -192,6 +277,29 @@ describe("Artifactly MCP tools", () => {
192
277
  rmSync(dir, { recursive: true, force: true });
193
278
  });
194
279
 
280
+ it("rejects conflicting markdown and content update arguments with a specific error", async () => {
281
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
282
+ const credentialPath = join(dir, "credentials.json");
283
+ writeCredentials(
284
+ {
285
+ apiBaseUrl: "https://artifactly.example",
286
+ apiKey: "art_t...et",
287
+ email: "agent@example.com",
288
+ workspace: "agent",
289
+ },
290
+ credentialPath,
291
+ );
292
+
293
+ await expect(
294
+ callArtifactlyMcpTool({
295
+ name: "artifactly_update_artifact",
296
+ credentialPath,
297
+ arguments: { artifactId: "artifact_1", markdown: "# Markdown", content: "Plain" },
298
+ }),
299
+ ).rejects.toThrow(/Use either markdown or content, not both/);
300
+ rmSync(dir, { recursive: true, force: true });
301
+ });
302
+
195
303
  it("publishes local HTML and PDF files through the artifact API", async () => {
196
304
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
197
305
  const credentialPath = join(dir, "credentials.json");
@@ -252,4 +360,61 @@ describe("Artifactly MCP tools", () => {
252
360
  ]);
253
361
  rmSync(dir, { recursive: true, force: true });
254
362
  });
363
+
364
+ it("uses explicit MIME type instead of extension when publishing files", async () => {
365
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
366
+ const credentialPath = join(dir, "credentials.json");
367
+ const markdownPath = join(dir, "report.md");
368
+ let publishedBody: unknown;
369
+ writeFileSync(markdownPath, "# Report");
370
+ writeCredentials(
371
+ {
372
+ apiBaseUrl: "https://artifactly.example",
373
+ apiKey: "art_t...et",
374
+ email: "agent@example.com",
375
+ workspace: "agent",
376
+ },
377
+ credentialPath,
378
+ );
379
+
380
+ await callArtifactlyMcpTool({
381
+ name: "artifactly_publish_file",
382
+ credentialPath,
383
+ arguments: { path: markdownPath, mimeType: "application/pdf" },
384
+ postJson: async (_path, body) => {
385
+ publishedBody = body;
386
+ return { id: "artifact_1", url: "https://artifactly.example/share/agent/report" };
387
+ },
388
+ });
389
+
390
+ expect(publishedBody).toMatchObject({
391
+ kind: "file",
392
+ storageUrl: "data:application/pdf;base64,IyBSZXBvcnQ=",
393
+ });
394
+ rmSync(dir, { recursive: true, force: true });
395
+ });
396
+
397
+ it("returns a friendly error when publishing a missing file", async () => {
398
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
399
+ const credentialPath = join(dir, "credentials.json");
400
+ writeCredentials(
401
+ {
402
+ apiBaseUrl: "https://artifactly.example",
403
+ apiKey: "art_test_secret",
404
+ email: "agent@example.com",
405
+ workspace: "agent",
406
+ },
407
+ credentialPath,
408
+ );
409
+
410
+ await expect(
411
+ callArtifactlyMcpTool({
412
+ name: "artifactly_publish_file",
413
+ credentialPath,
414
+ arguments: { path: join(dir, "missing.pdf") },
415
+ }),
416
+ ).rejects.toThrow(/File not found.*missing\.pdf/);
417
+
418
+ rmSync(dir, { recursive: true, force: true });
419
+ });
255
420
  });
@@ -65,6 +65,14 @@ function stringArg(
65
65
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
66
66
  }
67
67
 
68
+ function optionalStringArg(
69
+ args: Record<string, unknown> | undefined,
70
+ key: string,
71
+ ): string | undefined {
72
+ const value = args?.[key];
73
+ return typeof value === "string" ? value.trim() : undefined;
74
+ }
75
+
68
76
  function requireStringArg(
69
77
  args: Record<string, unknown> | undefined,
70
78
  key: string,
@@ -76,6 +84,17 @@ function requireStringArg(
76
84
  return value;
77
85
  }
78
86
 
87
+ function requireRawStringArg(
88
+ args: Record<string, unknown> | undefined,
89
+ key: string,
90
+ ): string {
91
+ const value = args?.[key];
92
+ if (typeof value !== "string" || !value.trim()) {
93
+ throw new Error(`Missing required argument: ${key}`);
94
+ }
95
+ return value;
96
+ }
97
+
79
98
  function stringArrayArg(
80
99
  args: Record<string, unknown> | undefined,
81
100
  key: string,
@@ -94,6 +113,28 @@ function objectResult(value: unknown): Record<string, unknown> {
94
113
  return value as Record<string, unknown>;
95
114
  }
96
115
 
116
+ const LOGIN_GUIDANCE =
117
+ "Artifactly credentials are not configured. Run artifactly_request_login_code first, then run artifactly_verify_login_code with the emailed code.";
118
+
119
+ function booleanArg(args: Record<string, unknown> | undefined, key: string): boolean {
120
+ return args?.[key] === true;
121
+ }
122
+
123
+ function readFileForPublish(path: string): Buffer {
124
+ try {
125
+ return readFileSync(path);
126
+ } catch (error) {
127
+ const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
128
+ if (code === "ENOENT") {
129
+ throw new Error(`File not found: ${path}`);
130
+ }
131
+ if (code === "EACCES" || code === "EPERM") {
132
+ throw new Error(`File is not readable: ${path}`);
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+
97
138
  function titleFromPath(path: string): string {
98
139
  const name = basename(path, extname(path));
99
140
  return name || "artifact";
@@ -109,12 +150,11 @@ function inferMimeType(path: string): string {
109
150
  return "application/octet-stream";
110
151
  }
111
152
 
112
- function kindForMime(path: string, mimeType: string) {
113
- const extension = extname(path).toLowerCase();
153
+ function kindForMime(_path: string, mimeType: string) {
114
154
  const normalized = mimeType.toLowerCase();
115
- if (normalized.includes("html") || extension === ".html" || extension === ".htm") return "html";
116
- if (normalized.includes("json") || extension === ".json") return "json";
117
- if (normalized.includes("markdown") || extension === ".md" || extension === ".markdown") return "markdown";
155
+ if (normalized.includes("html")) return "html";
156
+ if (normalized.includes("json")) return "json";
157
+ if (normalized.includes("markdown")) return "markdown";
118
158
  return "file";
119
159
  }
120
160
 
@@ -122,8 +162,9 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
122
162
  return [
123
163
  {
124
164
  name: "artifactly_auth_status",
125
- description: "Check whether local Artifactly credentials are configured.",
126
- inputSchema: { type: "object", properties: {} },
165
+ description:
166
+ "Check local Artifactly credentials. Pass validate: true to confirm the saved API key with the Artifactly server.",
167
+ inputSchema: { type: "object", properties: { validate: { type: "boolean" } } },
127
168
  },
128
169
  {
129
170
  name: "artifactly_request_login_code",
@@ -151,7 +192,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
151
192
  },
152
193
  {
153
194
  name: "artifactly_publish_markdown",
154
- description: "Publish markdown through the Artifactly API using stored credentials.",
195
+ description:
196
+ "Publish markdown through the Artifactly API using stored credentials. Defaults to unlisted visibility; visibility can be public, unlisted, private, or gated.",
155
197
  inputSchema: {
156
198
  type: "object",
157
199
  properties: {
@@ -178,7 +220,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
178
220
  },
179
221
  {
180
222
  name: "artifactly_update_artifact",
181
- description: "Update an existing Artifactly artifact, including visibility and gated access emails.",
223
+ description:
224
+ "Update an existing Artifactly artifact, including visibility and gated access emails. Use markdown or content, not both; description may be an empty string to clear it.",
182
225
  inputSchema: {
183
226
  type: "object",
184
227
  properties: {
@@ -204,7 +247,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
204
247
  },
205
248
  {
206
249
  name: "artifactly_publish_file",
207
- description: "Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API.",
250
+ description:
251
+ "Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API. Defaults to unlisted visibility; explicit mimeType takes precedence over file extension.",
208
252
  inputSchema: {
209
253
  type: "object",
210
254
  properties: {
@@ -227,14 +271,29 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
227
271
  const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
228
272
 
229
273
  if (input.name === "artifactly_auth_status") {
230
- return credentials
231
- ? {
232
- authenticated: true,
233
- apiBaseUrl: credentials.apiBaseUrl,
234
- email: credentials.email,
235
- workspace: credentials.workspace,
236
- }
237
- : { authenticated: false, apiBaseUrl };
274
+ if (!credentials) {
275
+ return { authenticated: false, apiBaseUrl };
276
+ }
277
+
278
+ const result: Record<string, unknown> = {
279
+ authenticated: true,
280
+ apiBaseUrl: credentials.apiBaseUrl,
281
+ email: credentials.email,
282
+ workspace: credentials.workspace,
283
+ };
284
+
285
+ if (booleanArg(input.arguments, "validate")) {
286
+ try {
287
+ await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey });
288
+ result.serverAuthenticated = true;
289
+ result.validationError = null;
290
+ } catch (error) {
291
+ result.serverAuthenticated = false;
292
+ result.validationError = error instanceof Error ? error.message : "Validation failed";
293
+ }
294
+ }
295
+
296
+ return result;
238
297
  }
239
298
 
240
299
  if (input.name === "artifactly_request_login_code") {
@@ -280,10 +339,10 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
280
339
 
281
340
  if (input.name === "artifactly_publish_markdown") {
282
341
  if (!credentials) {
283
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
342
+ throw new Error(LOGIN_GUIDANCE);
284
343
  }
285
344
  const title = requireStringArg(input.arguments, "title");
286
- const content = requireStringArg(input.arguments, "markdown");
345
+ const content = requireRawStringArg(input.arguments, "markdown");
287
346
  const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
288
347
  return objectResult(
289
348
  await postJson(
@@ -296,14 +355,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
296
355
 
297
356
  if (input.name === "artifactly_list_artifacts") {
298
357
  if (!credentials) {
299
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
358
+ throw new Error(LOGIN_GUIDANCE);
300
359
  }
301
360
  return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
302
361
  }
303
362
 
304
363
  if (input.name === "artifactly_get_artifact") {
305
364
  if (!credentials) {
306
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
365
+ throw new Error(LOGIN_GUIDANCE);
307
366
  }
308
367
  const artifactId = requireStringArg(input.arguments, "artifactId");
309
368
  return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
@@ -311,13 +370,16 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
311
370
 
312
371
  if (input.name === "artifactly_update_artifact") {
313
372
  if (!credentials) {
314
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
373
+ throw new Error(LOGIN_GUIDANCE);
315
374
  }
316
375
  const artifactId = requireStringArg(input.arguments, "artifactId");
317
376
  const title = stringArg(input.arguments, "title");
318
- const markdown = stringArg(input.arguments, "markdown");
319
- const content = stringArg(input.arguments, "content");
320
- const description = stringArg(input.arguments, "description");
377
+ const markdown = optionalStringArg(input.arguments, "markdown");
378
+ const content = optionalStringArg(input.arguments, "content");
379
+ if (markdown !== undefined && content !== undefined) {
380
+ throw new Error("Use either markdown or content, not both.");
381
+ }
382
+ const description = optionalStringArg(input.arguments, "description");
321
383
  const visibility = stringArg(input.arguments, "visibility");
322
384
  const accessList = stringArrayArg(input.arguments, "accessList");
323
385
  return objectResult(
@@ -328,7 +390,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
328
390
  ...(title ? { title } : {}),
329
391
  ...(markdown ? { markdown } : {}),
330
392
  ...(content ? { content } : {}),
331
- ...(description ? { description } : {}),
393
+ ...(description !== undefined ? { description } : {}),
332
394
  ...(visibility ? { visibility } : {}),
333
395
  ...(accessList ? { accessList } : {}),
334
396
  },
@@ -339,7 +401,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
339
401
 
340
402
  if (input.name === "artifactly_delete_artifact") {
341
403
  if (!credentials) {
342
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
404
+ throw new Error(LOGIN_GUIDANCE);
343
405
  }
344
406
  const artifactId = requireStringArg(input.arguments, "artifactId");
345
407
  return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
@@ -347,14 +409,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
347
409
 
348
410
  if (input.name === "artifactly_publish_file") {
349
411
  if (!credentials) {
350
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
412
+ throw new Error(LOGIN_GUIDANCE);
351
413
  }
352
414
  const path = requireStringArg(input.arguments, "path");
353
415
  const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
354
416
  const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
355
417
  const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
356
418
  const kind = kindForMime(path, mimeType);
357
- const file = readFileSync(path);
419
+ const file = readFileForPublish(path);
358
420
  const payload =
359
421
  kind === "file"
360
422
  ? {