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.
- package/dist/index.js +123 -9
- 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
|
-
"
|
|
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) => ``), ""] : [];
|
|
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.
|
|
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("
|
|
12615
|
-
|
|
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.
|
|
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.
|
|
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",
|