cloudtifact 0.1.2 → 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 +123 -9
  2. package/package.json +1 -1
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,6 +5703,7 @@ 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({
@@ -5839,7 +5846,9 @@ const workspaceInvitePreviewOutput = object({
5839
5846
  invitedBy: userSchema.nullable(),
5840
5847
  memberCount: number$1().int(),
5841
5848
  expiresAt: number$1(),
5842
- alreadyMember: boolean()
5849
+ alreadyMember: boolean(),
5850
+ email: string().nullable().default(null),
5851
+ wrongAccount: boolean().default(false)
5843
5852
  });
5844
5853
  const acceptWorkspaceInviteOutput = object({ workspace: myWorkspaceSchema });
5845
5854
  const auditActionSchema = _enum([
@@ -5855,6 +5864,8 @@ const auditActionSchema = _enum([
5855
5864
  "billing.checkout",
5856
5865
  "space.created",
5857
5866
  "space.updated",
5867
+ "space.moved",
5868
+ "space.deleted",
5858
5869
  "room.link_changed",
5859
5870
  "room.person_added",
5860
5871
  "room.person_changed",
@@ -5933,6 +5944,11 @@ object({
5933
5944
  viewers: viewersSchema.partial().optional(),
5934
5945
  password: string().min(4).max(200).nullable().optional()
5935
5946
  });
5947
+ object({ workspace: workspaceSlugSchema });
5948
+ const moveSpaceOutput = object({
5949
+ space: spaceSchema,
5950
+ moved: array(spaceIdSchema)
5951
+ });
5936
5952
  object({ password: string().min(1).max(200) });
5937
5953
  const unlockSpaceOutput = object({
5938
5954
  token: string(),
@@ -6113,6 +6129,12 @@ const guestIdentitySchema = object({
6113
6129
  name: string().trim().min(1).max(40),
6114
6130
  key: guestKeySchema
6115
6131
  });
6132
+ const commentImageSchema = object({
6133
+ id: string(),
6134
+ url: string(),
6135
+ contentType: string(),
6136
+ size: number$1().int()
6137
+ });
6116
6138
  const commentSchema = object({
6117
6139
  id: string(),
6118
6140
  path: string(),
@@ -6122,6 +6144,7 @@ const commentSchema = object({
6122
6144
  guestName: string().nullable(),
6123
6145
  origin: commentOriginSchema.nullable(),
6124
6146
  body: string(),
6147
+ images: array(commentImageSchema).default([]),
6125
6148
  quote: string().nullable(),
6126
6149
  anchor: anchorSchema.nullable(),
6127
6150
  via: string().nullable(),
@@ -6144,6 +6167,30 @@ object({
6144
6167
  ]).default("open")
6145
6168
  });
6146
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
+ });
6147
6194
  object({
6148
6195
  body: string().min(1).max(1e4),
6149
6196
  guest: object({ key: guestKeySchema }).optional()
@@ -6163,6 +6210,7 @@ object({
6163
6210
  anchor: anchorSchema.optional(),
6164
6211
  parentId: string().optional(),
6165
6212
  versionNumber: number$1().int().optional(),
6213
+ images: array(commentImageIdSchema).max(4).optional(),
6166
6214
  guest: guestIdentitySchema.optional()
6167
6215
  });
6168
6216
  const DB_MAX_SQL_BYTES = 102400;
@@ -6475,7 +6523,9 @@ const ownerLinkOutput = object({
6475
6523
  const planIdSchema = _enum([
6476
6524
  "free",
6477
6525
  "pro",
6526
+ "pro_annual",
6478
6527
  "team",
6528
+ "team20",
6479
6529
  "enterprise"
6480
6530
  ]);
6481
6531
  const usageShape = {
@@ -6508,7 +6558,9 @@ const billingOutput = object({
6508
6558
  object({ plan: _enum([
6509
6559
  "free",
6510
6560
  "pro",
6511
- "team"
6561
+ "pro_annual",
6562
+ "team",
6563
+ "team20"
6512
6564
  ]) });
6513
6565
  const checkoutOutput = object({ url: string() });
6514
6566
  const portalOutput = object({ url: string() });
@@ -6593,6 +6645,8 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6593
6645
  createSpace: (input) => call("POST", "/v1/spaces", spaceSchema, input),
6594
6646
  getSpace: (id) => call("GET", space(id), spaceSchema),
6595
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),
6596
6650
  head: (id) => call("GET", `${space(id)}/head`, headOutput),
6597
6651
  unlockSpace: (id, input) => call("POST", `${space(id)}/unlock`, unlockSpaceOutput, input),
6598
6652
  recordView: (id, input) => call("POST", `${space(id)}/views`, null, input),
@@ -6637,7 +6691,19 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6637
6691
  if (query.status) q.set("status", query.status);
6638
6692
  return call("GET", `${space(id)}/comments${q.size ? `?${q}` : ""}`, listCommentsOutput);
6639
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
+ },
6640
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
+ }),
6641
6707
  editComment: (commentId, input) => call("PATCH", `/v1/comments/${enc$1(commentId)}`, commentSchema, input),
6642
6708
  commentRevisions: (commentId) => call("GET", `/v1/comments/${enc$1(commentId)}/revisions`, commentRevisionsOutput),
6643
6709
  deleteComment: (commentId, input = {}) => call("DELETE", `/v1/comments/${enc$1(commentId)}`, null, input),
@@ -6674,6 +6740,13 @@ function createClient({ baseUrl, token, fetch: fetchImpl = fetch, agent, source,
6674
6740
  deleteMessage: (roomId, messageId) => send("DELETE", `${room(roomId)}/messages/${enc$1(messageId)}`),
6675
6741
  listSources: (id) => call("GET", `${room(id)}/sources`, listSourcesOutput),
6676
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
+ }),
6677
6750
  updateSource: (id, sourceId, input) => call("PATCH", `${room(id)}/sources/${enc$1(sourceId)}`, sourceSchema, input),
6678
6751
  deleteSource: (id, sourceId) => call("DELETE", `${room(id)}/sources/${enc$1(sourceId)}`, null),
6679
6752
  listApps: (query = {}) => {
@@ -10521,7 +10594,8 @@ function commentsData(threads, opts) {
10521
10594
  edited: c.editedBy ? `${c.editedBy.handle}${c.editedVia ? ` via ${c.editedVia}` : ""}` : null,
10522
10595
  status: t.resolvedAt === null ? "open" : "resolved",
10523
10596
  quote: c.quote,
10524
- body: c.body
10597
+ body: c.body,
10598
+ images: c.images?.length ? c.images.map((i) => i.url).join(" ") : null
10525
10599
  })));
10526
10600
  return {
10527
10601
  space: opts.spaceUrl ?? null,
@@ -11466,6 +11540,8 @@ const from = (c) => {
11466
11540
  };
11467
11541
  const who = (c) => (c.guestName ? `${c.guestName} (guest)` : `@${c.author.handle}${c.via ? ` via ${agentLabel(c.via)}` : ""}`) + from(c);
11468
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})`), ""] : [];
11469
11545
  /** Open threads as Markdown an agent can act on directly. */
11470
11546
  function threadsToMarkdown(threads, opts) {
11471
11547
  const spaceFlag = opts.defaultSpace ? "" : ` --space ${opts.spaceId}`;
@@ -11482,8 +11558,8 @@ function threadsToMarkdown(threads, opts) {
11482
11558
  out.push(`### Comment \`${t.id}\`${status}`, "");
11483
11559
  out.push(`${who(t)} on v${t.versionNumber}, ${when(t.createdAt)}${edited(t)}:`, "");
11484
11560
  if (t.quote) out.push("On this text:", "", quote(t.quote), "");
11485
- out.push(t.body.trim(), "");
11486
- 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));
11487
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}`, "```", "");
11488
11564
  }
11489
11565
  }
@@ -11840,6 +11916,10 @@ When you make an HTML page for the user, run \`cloudtifact publish <file.html>\`
11840
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.
11841
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.
11842
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
+
11843
11923
  ## Workspaces
11844
11924
 
11845
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.
@@ -12009,6 +12089,13 @@ function findUpload(uploads, ref) {
12009
12089
  const names = uploads.slice(0, 10).map((u) => ` ${u.name} (${u.id})`);
12010
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.");
12011
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
+ }
12012
12099
  /**
12013
12100
  * Streams a download to `dest` (via a temp file, renamed at the end), checking the SHA-256 the API sent.
12014
12101
  * Refuses to overwrite unless `force`.
@@ -12116,7 +12203,7 @@ function workspaceTextLines(workspaces, currentSlug) {
12116
12203
  }
12117
12204
  //#endregion
12118
12205
  //#region src/program.ts
12119
- const VERSION = "0.1.2";
12206
+ const VERSION = "0.1.3";
12120
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);
12121
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) => {
12122
12209
  const config = await readGlobalConfig();
@@ -12611,13 +12698,40 @@ program.command("uploads").description("List the files uploaded to a space, newe
12611
12698
  if (!rows.length) return console.log("No uploads yet. Add some with `cloudtifact upload <files...>`.");
12612
12699
  for (const r of rows) console.log(`${r.id} ${r.at} UTC ${(r.size / 1024 / 1024).toFixed(1).padStart(6)} MB ${r.name}`);
12613
12700
  }));
12614
- 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) => {
12615
- 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>.");
12616
12726
  const { spaceId } = await pickSpace(opts.space, { noRepo: "Pass --space <id>, or run inside a linked repo." });
12617
12727
  const { client } = await getClient({ auth: false });
12618
12728
  let res;
12619
12729
  let fallback;
12620
- 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) {
12621
12735
  const v = opts.version ? Number(opts.version) : void 0;
12622
12736
  if (v !== void 0 && !(Number.isInteger(v) && v > 0)) throw new CliError("--version must be a positive integer.");
12623
12737
  res = await client.downloadFile(spaceId, opts.file, v);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudtifact",
3
- "version": "0.1.2",
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",