cloudtifact 0.1.1 → 0.1.3

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 (2) hide show
  1. package/dist/index.js +170 -22
  2. package/package.json +11 -12
package/dist/index.js CHANGED
@@ -5618,6 +5618,12 @@ function datetime(params) {
5618
5618
  function number(params) {
5619
5619
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
5620
5620
  }
5621
+ //#endregion
5622
+ //#region ../contract/src/comment-images.ts
5623
+ /** A guest uploading an image sends their guest key under this header (the body is the image). */
5624
+ const GUEST_KEY_HEADER = "x-cloudtifact-guest-key";
5625
+ /** 128 random bits, hex. */
5626
+ const commentImageIdSchema = string().regex(/^[0-9a-f]{32}$/);
5621
5627
  new TextEncoder();
5622
5628
  //#endregion
5623
5629
  //#region ../contract/src/schemas.ts
@@ -5697,12 +5703,18 @@ const errorSchema = object({ error: object({
5697
5703
  object({
5698
5704
  handle: handleSchema,
5699
5705
  name: string().max(100).optional(),
5706
+ email: email().max(200).optional(),
5700
5707
  label: _enum(["cli", "web"]).default("cli")
5701
5708
  });
5702
5709
  const devLoginOutput = object({
5703
5710
  token: string(),
5704
5711
  user: userSchema
5705
5712
  });
5713
+ /** A one-hour token for the comment panel on published pages: reads a space and reads or posts its comments. */
5714
+ const embedTokenOutput = object({
5715
+ token: string(),
5716
+ expiresAt: number$1()
5717
+ });
5706
5718
  object({ label: string().max(60).optional() });
5707
5719
  const cliLoginStartOutput = object({
5708
5720
  deviceCode: string(),
@@ -5749,6 +5761,25 @@ const listTokensOutput = object({ tokens: array(object({
5749
5761
  const preferencesSchema = object({ developerMode: boolean() });
5750
5762
  preferencesSchema.partial();
5751
5763
  object({ handle: handleSchema });
5764
+ const emailCategoryIdSchema = _enum([
5765
+ "invites",
5766
+ "activity",
5767
+ "product"
5768
+ ]);
5769
+ const emailPreferencesSchema = object({
5770
+ email: string().nullable(),
5771
+ categories: array(object({
5772
+ id: emailCategoryIdSchema,
5773
+ label: string(),
5774
+ description: string(),
5775
+ enabled: boolean()
5776
+ }))
5777
+ });
5778
+ object({
5779
+ invites: boolean(),
5780
+ activity: boolean(),
5781
+ product: boolean()
5782
+ }).partial().strict();
5752
5783
  const meOutput = object({
5753
5784
  user: userSchema,
5754
5785
  workspaces: array(myWorkspaceSchema),
@@ -5815,7 +5846,9 @@ const workspaceInvitePreviewOutput = object({
5815
5846
  invitedBy: userSchema.nullable(),
5816
5847
  memberCount: number$1().int(),
5817
5848
  expiresAt: number$1(),
5818
- alreadyMember: boolean()
5849
+ alreadyMember: boolean(),
5850
+ email: string().nullable().default(null),
5851
+ wrongAccount: boolean().default(false)
5819
5852
  });
5820
5853
  const acceptWorkspaceInviteOutput = object({ workspace: myWorkspaceSchema });
5821
5854
  const auditActionSchema = _enum([
@@ -5831,6 +5864,8 @@ const auditActionSchema = _enum([
5831
5864
  "billing.checkout",
5832
5865
  "space.created",
5833
5866
  "space.updated",
5867
+ "space.moved",
5868
+ "space.deleted",
5834
5869
  "room.link_changed",
5835
5870
  "room.person_added",
5836
5871
  "room.person_changed",
@@ -5909,6 +5944,11 @@ object({
5909
5944
  viewers: viewersSchema.partial().optional(),
5910
5945
  password: string().min(4).max(200).nullable().optional()
5911
5946
  });
5947
+ object({ workspace: workspaceSlugSchema });
5948
+ const moveSpaceOutput = object({
5949
+ space: spaceSchema,
5950
+ moved: array(spaceIdSchema)
5951
+ });
5912
5952
  object({ password: string().min(1).max(200) });
5913
5953
  const unlockSpaceOutput = object({
5914
5954
  token: string(),
@@ -6089,6 +6129,12 @@ const guestIdentitySchema = object({
6089
6129
  name: string().trim().min(1).max(40),
6090
6130
  key: guestKeySchema
6091
6131
  });
6132
+ const commentImageSchema = object({
6133
+ id: string(),
6134
+ url: string(),
6135
+ contentType: string(),
6136
+ size: number$1().int()
6137
+ });
6092
6138
  const commentSchema = object({
6093
6139
  id: string(),
6094
6140
  path: string(),
@@ -6098,6 +6144,7 @@ const commentSchema = object({
6098
6144
  guestName: string().nullable(),
6099
6145
  origin: commentOriginSchema.nullable(),
6100
6146
  body: string(),
6147
+ images: array(commentImageSchema).default([]),
6101
6148
  quote: string().nullable(),
6102
6149
  anchor: anchorSchema.nullable(),
6103
6150
  via: string().nullable(),
@@ -6120,6 +6167,30 @@ object({
6120
6167
  ]).default("open")
6121
6168
  });
6122
6169
  const listCommentsOutput = object({ threads: array(threadSchema) });
6170
+ object({
6171
+ before: number().int().positive().optional(),
6172
+ limit: number().int().min(1).max(50).default(20)
6173
+ });
6174
+ const waitingOutput = object({
6175
+ threads: array(object({
6176
+ thread: threadSchema,
6177
+ space: object({
6178
+ id: string(),
6179
+ name: string(),
6180
+ path: string(),
6181
+ kind: _enum([
6182
+ "repo",
6183
+ "room",
6184
+ "app"
6185
+ ])
6186
+ }),
6187
+ reason: _enum(["participant", "owner"]),
6188
+ lastAt: number$1()
6189
+ })),
6190
+ total: number$1().int(),
6191
+ spaces: number$1().int(),
6192
+ next: number$1().nullable()
6193
+ });
6123
6194
  object({
6124
6195
  body: string().min(1).max(1e4),
6125
6196
  guest: object({ key: guestKeySchema }).optional()
@@ -6139,6 +6210,7 @@ object({
6139
6210
  anchor: anchorSchema.optional(),
6140
6211
  parentId: string().optional(),
6141
6212
  versionNumber: number$1().int().optional(),
6213
+ images: array(commentImageIdSchema).max(4).optional(),
6142
6214
  guest: guestIdentitySchema.optional()
6143
6215
  });
6144
6216
  const DB_MAX_SQL_BYTES = 102400;
@@ -6451,7 +6523,9 @@ const ownerLinkOutput = object({
6451
6523
  const planIdSchema = _enum([
6452
6524
  "free",
6453
6525
  "pro",
6526
+ "pro_annual",
6454
6527
  "team",
6528
+ "team20",
6455
6529
  "enterprise"
6456
6530
  ]);
6457
6531
  const usageShape = {
@@ -6484,7 +6558,9 @@ const billingOutput = object({
6484
6558
  object({ plan: _enum([
6485
6559
  "free",
6486
6560
  "pro",
6487
- "team"
6561
+ "pro_annual",
6562
+ "team",
6563
+ "team20"
6488
6564
  ]) });
6489
6565
  const checkoutOutput = object({ url: string() });
6490
6566
  const portalOutput = object({ url: string() });
@@ -6538,8 +6614,11 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6538
6614
  const app = (id) => `/v1/apps/${enc$1(id)}`;
6539
6615
  return {
6540
6616
  devLogin: (input) => call("POST", "/v1/auth/dev-login", devLoginOutput, input),
6617
+ createEmbedToken: () => call("POST", "/v1/auth/embed-token", embedTokenOutput),
6541
6618
  me: () => call("GET", "/v1/me", meOutput),
6542
6619
  updatePreferences: (input) => call("PATCH", "/v1/me/preferences", preferencesSchema, input),
6620
+ emailPreferences: () => call("GET", "/v1/me/email-preferences", emailPreferencesSchema),
6621
+ updateEmailPreferences: (input) => call("PATCH", "/v1/me/email-preferences", emailPreferencesSchema, input),
6543
6622
  /** Changes your handle; your personal workspace's slug follows when it matched the old handle. */
6544
6623
  updateHandle: (input) => call("PATCH", "/v1/me/handle", userSchema, input),
6545
6624
  /** Deletes your account for good: tokens stop working, workspaces only you are in are deleted. */
@@ -6566,6 +6645,8 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6566
6645
  createSpace: (input) => call("POST", "/v1/spaces", spaceSchema, input),
6567
6646
  getSpace: (id) => call("GET", space(id), spaceSchema),
6568
6647
  updateSpace: (id, input) => call("PATCH", space(id), spaceSchema, input),
6648
+ deleteSpace: (id) => call("DELETE", space(id), null),
6649
+ moveSpace: (id, input) => call("POST", `${space(id)}/move`, moveSpaceOutput, input),
6569
6650
  head: (id) => call("GET", `${space(id)}/head`, headOutput),
6570
6651
  unlockSpace: (id, input) => call("POST", `${space(id)}/unlock`, unlockSpaceOutput, input),
6571
6652
  recordView: (id, input) => call("POST", `${space(id)}/views`, null, input),
@@ -6610,7 +6691,19 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6610
6691
  if (query.status) q.set("status", query.status);
6611
6692
  return call("GET", `${space(id)}/comments${q.size ? `?${q}` : ""}`, listCommentsOutput);
6612
6693
  },
6694
+ waiting: (slug, query = {}) => {
6695
+ const q = new URLSearchParams();
6696
+ if (query.before) q.set("before", String(query.before));
6697
+ if (query.limit) q.set("limit", String(query.limit));
6698
+ return call("GET", `/v1/workspaces/${enc$1(slug)}/waiting${q.size ? `?${q}` : ""}`, waitingOutput);
6699
+ },
6613
6700
  createComment: (id, input) => call("POST", `${space(id)}/comments`, commentSchema, input),
6701
+ /** Uploads an image for a comment; post it with createComment's `images`. A guest passes their key. */
6702
+ uploadCommentImage: (id, body, opts) => call("POST", `${space(id)}/comment-images`, commentImageSchema, body, {
6703
+ "content-type": opts.contentType || "application/octet-stream",
6704
+ "content-length": String(opts.size),
6705
+ ...opts.guestKey ? { [GUEST_KEY_HEADER]: opts.guestKey } : {}
6706
+ }),
6614
6707
  editComment: (commentId, input) => call("PATCH", `/v1/comments/${enc$1(commentId)}`, commentSchema, input),
6615
6708
  commentRevisions: (commentId) => call("GET", `/v1/comments/${enc$1(commentId)}/revisions`, commentRevisionsOutput),
6616
6709
  deleteComment: (commentId, input = {}) => call("DELETE", `/v1/comments/${enc$1(commentId)}`, null, input),
@@ -6647,6 +6740,13 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6647
6740
  deleteMessage: (roomId, messageId) => send("DELETE", `${room(roomId)}/messages/${enc$1(messageId)}`),
6648
6741
  listSources: (id) => call("GET", `${room(id)}/sources`, listSourcesOutput),
6649
6742
  uploadSource: (id, name, bytes, contentType) => call("POST", `${room(id)}/sources?name=${enc$1(name)}`, sourceSchema, bytes, { "content-type": contentType }),
6743
+ /** A source file's bytes as an attachment (the raw Response, to stream or save). */
6744
+ downloadSource: (id, sourceId) => request("GET", `${room(id)}/sources/${enc$1(sourceId)}/raw`),
6745
+ /** Uploads an image for an image block in the room's doc; put its `url` in the block's src. */
6746
+ uploadRoomImage: (id, body, opts) => call("POST", `${room(id)}/images`, commentImageSchema, body, {
6747
+ "content-type": opts.contentType || "application/octet-stream",
6748
+ "content-length": String(opts.size)
6749
+ }),
6650
6750
  updateSource: (id, sourceId, input) => call("PATCH", `${room(id)}/sources/${enc$1(sourceId)}`, sourceSchema, input),
6651
6751
  deleteSource: (id, sourceId) => call("DELETE", `${room(id)}/sources/${enc$1(sourceId)}`, null),
6652
6752
  listApps: (query = {}) => {
@@ -6725,8 +6825,8 @@ function urlsFor(domain, userContent = USER_CONTENT_DOMAIN) {
6725
6825
  };
6726
6826
  }
6727
6827
  const PUBLIC_URLS = urlsFor(PUBLIC_DOMAIN);
6728
- /** The one-line install for the CLI, against a given API origin. */
6729
- const installCommand = (apiUrl = PUBLIC_URLS.api) => `curl -fsSL ${apiUrl.replace(/\/+$/, "")}/install.sh | sh`;
6828
+ /** The one-line install for the CLI. */
6829
+ const installCommand = () => "npm i -g cloudtifact";
6730
6830
  //#endregion
6731
6831
  //#region ../contract/src/guest-writes.ts
6732
6832
  /**
@@ -10494,7 +10594,8 @@ function commentsData(threads, opts) {
10494
10594
  edited: c.editedBy ? `${c.editedBy.handle}${c.editedVia ? ` via ${c.editedVia}` : ""}` : null,
10495
10595
  status: t.resolvedAt === null ? "open" : "resolved",
10496
10596
  quote: c.quote,
10497
- body: c.body
10597
+ body: c.body,
10598
+ images: c.images?.length ? c.images.map((i) => i.url).join(" ") : null
10498
10599
  })));
10499
10600
  return {
10500
10601
  space: opts.spaceUrl ?? null,
@@ -11117,6 +11218,19 @@ function errorText(e) {
11117
11218
  //#endregion
11118
11219
  //#region src/publish.ts
11119
11220
  const ENTRY = "index.html";
11221
+ /** A public page's link with the comment panel already open (cloudtifact.app/<id>/?comments). */
11222
+ function commentsUrl(shareUrl) {
11223
+ return `${shareUrl}${shareUrl.includes("?") ? "&" : "?"}comments`;
11224
+ }
11225
+ /**
11226
+ * What `publish` prints after the version line. A public page gets two links to the same page: on its own (a micro
11227
+ * app, with a Comment button in the corner) and with the comment panel already open. A private page has only the
11228
+ * app link, which members open inside the Cloudtifact viewer.
11229
+ */
11230
+ function linkLines(space) {
11231
+ if (space.shareUrl) return [`Page: ${space.shareUrl}`, `With comments: ${commentsUrl(space.shareUrl)}`];
11232
+ return [`Link: ${space.url}`, `Private: only members of the ${space.workspace.slug} workspace can open it. To make it public, change visibility in the space settings on the website.`];
11233
+ }
11120
11234
  /** A local, relative reference, or null for absolute URLs, data:, anchors, etc. Drops ?query and #hash. */
11121
11235
  function localRef(raw) {
11122
11236
  const ref = raw.trim().replace(/^['"]|['"]$/g, "");
@@ -11426,6 +11540,8 @@ const from = (c) => {
11426
11540
  };
11427
11541
  const who = (c) => (c.guestName ? `${c.guestName} (guest)` : `@${c.author.handle}${c.via ? ` via ${agentLabel(c.via)}` : ""}`) + from(c);
11428
11542
  const edited = (c) => c.editedBy ? ` (edited by @${c.editedBy.handle}${c.editedVia ? ` via ${agentLabel(c.editedVia)}` : ""}; \`cloudtifact history ${c.id}\`)` : "";
11543
+ /** A comment's images as Markdown image links (an agent can fetch them), then a blank line; nothing if none. */
11544
+ const images = (c) => c.images?.length ? [...c.images.map((img, i) => `![Image ${i + 1}](${img.url})`), ""] : [];
11429
11545
  /** Open threads as Markdown an agent can act on directly. */
11430
11546
  function threadsToMarkdown(threads, opts) {
11431
11547
  const spaceFlag = opts.defaultSpace ? "" : ` --space ${opts.spaceId}`;
@@ -11442,8 +11558,8 @@ function threadsToMarkdown(threads, opts) {
11442
11558
  out.push(`### Comment \`${t.id}\`${status}`, "");
11443
11559
  out.push(`${who(t)} on v${t.versionNumber}, ${when(t.createdAt)}${edited(t)}:`, "");
11444
11560
  if (t.quote) out.push("On this text:", "", quote(t.quote), "");
11445
- out.push(t.body.trim(), "");
11446
- for (const r of t.replies) out.push(`- Reply \`${r.id}\` from ${who(r)} (${when(r.createdAt)})${edited(r)}:`, "", r.body.trim().replace(/^/gm, " "), "");
11561
+ out.push(t.body.trim(), "", ...images(t));
11562
+ for (const r of t.replies) out.push(`- Reply \`${r.id}\` from ${who(r)} (${when(r.createdAt)})${edited(r)}:`, "", r.body.trim().replace(/^/gm, " "), "", ...images(r).map((l) => l ? ` ${l}` : l));
11447
11563
  out.push("To answer and close it:", "", "```sh", `cloudtifact reply ${t.id}${spaceFlag} "<what you changed>"`, t.resolvedAt ? `cloudtifact unresolve ${t.id}` : `cloudtifact resolve ${t.id}`, "```", "");
11448
11564
  }
11449
11565
  }
@@ -11771,7 +11887,7 @@ description: Use when you make an HTML page for the user (publish it and share t
11771
11887
 
11772
11888
  The folder named by \`dir\` in \`.cloudtifact.json\` (at the repo root) is published to Cloudtifact. Every push makes a new version; readers comment on the published pages.
11773
11889
 
11774
- If the \`cloudtifact\` command isn't found, install it: \`${installCommand()}\` (it lands in \`~/.cloudtifact/bin\`). \`cloudtifact upgrade\` updates it to the latest version.
11890
+ If the \`cloudtifact\` command isn't found, install it: \`${installCommand()}\` (needs Node.js 20 or later). \`cloudtifact upgrade\` updates it to the latest version.
11775
11891
 
11776
11892
  ## Before editing docs in that folder
11777
11893
 
@@ -11794,12 +11910,16 @@ Published file types: Markdown, HTML, and the files pages load (CSS, JS, JSON, i
11794
11910
 
11795
11911
  ## Publishing HTML pages
11796
11912
 
11797
- When you make an HTML page for the user, run \`cloudtifact publish <file.html>\` and give them the link it prints. It uploads the page plus the local files it references (CSS, JS, images).
11913
+ When you make an HTML page for the user, run \`cloudtifact publish <file.html>\` and give them the links it prints. A public page prints two links to the same page: \`Page:\` opens it on its own (with a Comment button in the corner), and \`With comments:\` opens it with the comment panel already open. Give both, or the one the user asked for. A private page prints one \`Link:\`, for members of the workspace. It uploads the page plus the local files it references (CSS, JS, images).
11798
11914
 
11799
11915
  - To update it, edit the same file and publish it again: new version, same link.
11800
11916
  - Keep supporting files next to the HTML (or in subfolders) and reference them with relative paths (\`style.css\`, \`img/logo.png\`). Absolute paths and files outside the page's folder aren't included.
11801
11917
  - Pages are private by default; add \`--public\` on the first publish for a link anyone can open. \`cloudtifact artifacts\` lists what you've published.
11802
11918
 
11919
+ ## Space files
11920
+
11921
+ People can add files to a space for the work (chat exports, emails, PDFs, spreadsheets, screenshots and other images). \`cloudtifact sources --space <id> --toon\` lists them; \`cloudtifact download --source "<name or id>" --space <id>\` saves one locally so you can read it. Open downloaded images to look at them.
11922
+
11803
11923
  ## Workspaces
11804
11924
 
11805
11925
  Spaces live in workspaces (personal or team; the user can belong to several). \`cloudtifact publish\` and \`cloudtifact init\` create new spaces in the current workspace and print which one; pass \`--workspace <slug>\` to pick another. \`cloudtifact workspace list\` shows the user's workspaces (current one marked); \`cloudtifact workspace use <slug>\` switches the default. Pushing to an existing space doesn't need a workspace. Only change the default when the user asks.
@@ -11969,6 +12089,13 @@ function findUpload(uploads, ref) {
11969
12089
  const names = uploads.slice(0, 10).map((u) => ` ${u.name} (${u.id})`);
11970
12090
  throw new CliError(uploads.length ? `No upload named or with id "${ref}". Uploads in this space:\n${names.join("\n")}` : "This space has no uploads.");
11971
12091
  }
12092
+ /** A file in a space's "Files the AI reads" by id, else the newest one with that exact name. */
12093
+ function findSource(sources, ref) {
12094
+ const hit = sources.find((s) => s.id === ref) ?? sources.find((s) => s.name === ref);
12095
+ if (hit) return hit;
12096
+ const names = sources.slice(0, 10).map((s) => ` ${s.name} (${s.id})`);
12097
+ throw new CliError(sources.length ? `No file named or with id "${ref}" in this space. Its files:\n${names.join("\n")}` : "This space has no files for the AI.");
12098
+ }
11972
12099
  /**
11973
12100
  * Streams a download to `dest` (via a temp file, renamed at the end), checking the SHA-256 the API sent.
11974
12101
  * Refuses to overwrite unless `force`.
@@ -12076,7 +12203,7 @@ function workspaceTextLines(workspaces, currentSlug) {
12076
12203
  }
12077
12204
  //#endregion
12078
12205
  //#region src/program.ts
12079
- const VERSION = "0.1.1";
12206
+ const VERSION = "0.1.3";
12080
12207
  const program = new Command().name("cloudtifact").description("Publish pages and docs to Cloudtifact as links people comment on, with every version kept and the comments handed back to your agent.").version(VERSION);
12081
12208
  program.command("login").description("Sign in through your browser (GitHub, Google or email) and save a token to the global config").option("--no-browser", "don't open a browser, just print the URL (automatic over SSH)").action(run(async (opts) => {
12082
12209
  const config = await readGlobalConfig();
@@ -12460,13 +12587,6 @@ for (const [name, verb] of [["resolve", "Resolved"], ["unresolve", "Reopened"]])
12460
12587
  const c = await (name === "resolve" ? client.resolveComment(commentId) : client.unresolveComment(commentId));
12461
12588
  console.log(`${verb} ${c.id} on ${c.path}.`);
12462
12589
  }));
12463
- function printLink(space) {
12464
- if (space.shareUrl) console.log(space.shareUrl);
12465
- else {
12466
- console.log(space.url);
12467
- console.log(`Private: only members of the ${space.workspace.slug} workspace can open it. To make it public, change visibility in the space settings on the website.`);
12468
- }
12469
- }
12470
12590
  program.command("publish").description("Publish an HTML page (with the files it references) or a folder; prints its link").argument("<path>", "an .html file or a folder with index.html").option("--public", "anyone with the link can view (only when first published; default: private)").option("--name <name>", "space name (default: the page's <title>, else the file name)").option("--workspace <slug>", "workspace for a new space (default: the current one, see `workspace list`)").option("--entry <file>", "folder only: the page to publish as index.html").option("-m, --message <message>", "version message").option("--no-db", "don't run the db/*.sql files next to the page").action(run(async (target, opts) => {
12471
12591
  const abs = path.resolve(target);
12472
12592
  if (!existsSync(abs)) throw new CliError(`${target} does not exist.`);
@@ -12512,7 +12632,7 @@ program.command("publish").description("Publish an HTML page (with the files it
12512
12632
  const n = bundle.files.length;
12513
12633
  if (r.status === "pushed") console.log(`Published "${space.name}" v${r.version} (${n} file${n === 1 ? "" : "s"}; ${summarize(r.diff)})`);
12514
12634
  else console.log(`No changes since v${space.latestVersion}: "${space.name}"`);
12515
- printLink(space);
12635
+ for (const line of linkLines(space)) console.log(line);
12516
12636
  }));
12517
12637
  program.command("artifacts").description("List pages published with `cloudtifact publish`").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
12518
12638
  const map = await readArtifacts();
@@ -12528,7 +12648,8 @@ program.command("artifacts").description("List pages published with `cloudtifact
12528
12648
  workspace: space?.workspace.slug ?? null,
12529
12649
  visibility: space?.visibility ?? null,
12530
12650
  latestVersion: space?.latestVersion ?? null,
12531
- link: space ? space.shareUrl ?? space.url : null
12651
+ link: space ? space.shareUrl ?? space.url : null,
12652
+ commentsLink: space ? space.shareUrl ? commentsUrl(space.shareUrl) : space.url : null
12532
12653
  });
12533
12654
  }
12534
12655
  const format = resolveFormat(opts);
@@ -12577,13 +12698,40 @@ program.command("uploads").description("List the files uploaded to a space, newe
12577
12698
  if (!rows.length) return console.log("No uploads yet. Add some with `cloudtifact upload <files...>`.");
12578
12699
  for (const r of rows) console.log(`${r.id} ${r.at} UTC ${(r.size / 1024 / 1024).toFixed(1).padStart(6)} MB ${r.name}`);
12579
12700
  }));
12580
- program.command("download").description("Download an uploaded file (by name or id), or a pushed file with --file").argument("[name]", "the upload's name or id").option("--file <path>", "download a pushed file instead (path in the space)").option("--version <n>", "with --file: the version to download from (default: latest)").option("-o, --output <path>", "where to save it (default: the file's name, in the current folder)").option("--force", "overwrite an existing file").option("--space <id>", "space id or link (default: the first target in .cloudtifact.json)").action(run(async (name, opts) => {
12581
- if (!name && !opts.file) throw new CliError("Name an upload (see `cloudtifact uploads`), or pass --file <path>.");
12701
+ program.command("sources").description("List a space's \"Files the AI reads\" (chats, emails, PDFs, images...); get one with download --source").option("--space <id>", "space id or link (default: the first target in .cloudtifact.json)").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
12702
+ const { spaceId } = await pickSpace(opts.space, { noRepo: "Pass --space <id>, or run inside a linked repo." });
12703
+ const { client } = await getClient({ auth: false });
12704
+ const { sources } = await client.listSources(spaceId);
12705
+ const rows = sources.map((s) => ({
12706
+ id: s.id,
12707
+ name: s.name,
12708
+ type: s.contentType,
12709
+ size: s.size,
12710
+ status: s.status,
12711
+ summary: s.summary,
12712
+ by: s.uploadedBy?.handle ?? null,
12713
+ at: new Date(s.createdAt).toISOString().slice(0, 16).replace("T", " ")
12714
+ }));
12715
+ const format = resolveFormat(opts);
12716
+ if (format !== "text") return printData(format, {
12717
+ spaceId,
12718
+ sources: rows
12719
+ });
12720
+ if (!rows.length) return console.log("No files yet. People add them in the space's Files panel.");
12721
+ for (const r of rows) console.log(`${r.id} ${r.at} UTC ${(r.size / 1024 / 1024).toFixed(1).padStart(6)} MB ${r.name} (${r.summary ?? r.status})`);
12722
+ console.log("\nDownload one with `cloudtifact download --source \"<name or id>\"`.");
12723
+ }));
12724
+ program.command("download").description("Download an uploaded file (by name or id), a pushed file with --file, or a space file with --source").argument("[name]", "the upload's name or id").option("--file <path>", "download a pushed file instead (path in the space)").option("--source <name>", "download one of the space's \"Files the AI reads\" instead (name or id; see `sources`)").option("--version <n>", "with --file: the version to download from (default: latest)").option("-o, --output <path>", "where to save it (default: the file's name, in the current folder)").option("--force", "overwrite an existing file").option("--space <id>", "space id or link (default: the first target in .cloudtifact.json)").action(run(async (name, opts) => {
12725
+ if (!name && !opts.file && !opts.source) throw new CliError("Name an upload (see `cloudtifact uploads`), or pass --file <path> or --source <name>.");
12582
12726
  const { spaceId } = await pickSpace(opts.space, { noRepo: "Pass --space <id>, or run inside a linked repo." });
12583
12727
  const { client } = await getClient({ auth: false });
12584
12728
  let res;
12585
12729
  let fallback;
12586
- if (opts.file) {
12730
+ if (opts.source) {
12731
+ const source = findSource((await client.listSources(spaceId)).sources, opts.source);
12732
+ res = await client.downloadSource(spaceId, source.id);
12733
+ fallback = source.name;
12734
+ } else if (opts.file) {
12587
12735
  const v = opts.version ? Number(opts.version) : void 0;
12588
12736
  if (v !== void 0 && !(Number.isInteger(v) && v > 0)) throw new CliError("--version must be a positive integer.");
12589
12737
  res = await client.downloadFile(spaceId, opts.file, v);
package/package.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "cloudtifact",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Publish pages and docs to Cloudtifact as links people comment on, with every version kept and the comments handed back to your agent.",
5
5
  "license": "MIT",
6
6
  "author": "The Cloudtifact authors",
7
7
  "homepage": "https://cloudtifact.com/docs/reference/cli",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/cloudtifact/cloudtifact.git",
11
- "directory": "packages/cli"
12
- },
13
8
  "bugs": {
14
- "url": "https://github.com/cloudtifact/cloudtifact/issues"
9
+ "url": "https://cloudtifact.com/support"
15
10
  },
16
11
  "keywords": [
17
12
  "cloudtifact",
@@ -37,15 +32,15 @@
37
32
  "node": ">=20"
38
33
  },
39
34
  "devDependencies": {
35
+ "@cloudtifact/contract": "0.1.0",
36
+ "@cloudtifact/typescript-config": "0.1.0",
37
+ "@toon-format/toon": "^4.1.1",
40
38
  "@types/node": "^20.19.0",
41
39
  "commander": "^15.0.0",
42
40
  "tsdown": "^0.23.0",
43
41
  "typescript": "^5.9.3",
44
42
  "vitest": "~4.1.11",
45
- "zod": "^4.6.5",
46
- "@toon-format/toon": "^4.1.1",
47
- "@cloudtifact/contract": "0.1.0",
48
- "@cloudtifact/typescript-config": "0.1.0"
43
+ "zod": "^4.6.5"
49
44
  },
50
45
  "publishConfig": {
51
46
  "access": "public"
@@ -55,5 +50,9 @@
55
50
  "dev": "tsdown --watch",
56
51
  "typecheck": "tsc --noEmit",
57
52
  "test": "vitest run"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/Cloudtifact/cloudtifact-cli.git"
58
57
  }
59
- }
58
+ }