@gobi-ai/cli 2.0.29 → 2.0.31
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/attachments.js +35 -11
- package/dist/client.js +3 -0
- package/dist/commands/global.js +49 -7
- package/dist/commands/init.js +10 -1
- package/dist/commands/personal.js +49 -7
- package/dist/commands/space.js +147 -8
- package/dist/commands/sync.js +20 -2
- package/dist/commands/utils.js +70 -0
- package/dist/commands/vault.js +15 -3
- package/package.json +1 -1
- package/skills/gobi-artifact/SKILL.md +2 -2
- package/skills/gobi-core/SKILL.md +2 -2
- package/skills/gobi-core/references/space.md +5 -5
- package/skills/gobi-homepage/SKILL.md +19 -6
- package/skills/gobi-media/SKILL.md +2 -2
- package/skills/gobi-sense/SKILL.md +2 -2
- package/skills/gobi-space/SKILL.md +27 -8
- package/skills/gobi-space/references/global.md +30 -5
- package/skills/gobi-space/references/personal.md +30 -5
- package/skills/gobi-space/references/space.md +91 -20
- package/skills/gobi-vault/SKILL.md +3 -3
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"name": "gobi-ai"
|
|
5
5
|
},
|
|
6
6
|
"description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
|
|
7
|
-
"version": "2.0.
|
|
7
|
+
"version": "2.0.31",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "gobi",
|
|
11
11
|
"description": "Manage the Gobi collaborative knowledge platform from the command line. Publish vault profiles, create posts and replies, generate images and videos.",
|
|
12
|
-
"version": "2.0.
|
|
12
|
+
"version": "2.0.31",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/attachments.js
CHANGED
|
@@ -4,10 +4,13 @@ import { basename, join, extname, isAbsolute, resolve } from "path";
|
|
|
4
4
|
import ignore from "ignore";
|
|
5
5
|
import { WEBDRIVE_BASE_URL } from "./constants.js";
|
|
6
6
|
import { apiPost } from "./client.js";
|
|
7
|
+
import { normalizeSyncPattern } from "./commands/sync.js";
|
|
7
8
|
// Best-effort extension → MIME mapping. Anything we don't recognize falls
|
|
8
9
|
// back to `application/octet-stream`; the backend caps size per content-type
|
|
9
|
-
// tier (
|
|
10
|
-
// allowed. We're just trying to set a usable
|
|
10
|
+
// tier (10MB photos / 15MB GIFs / 512MB video / 250MB document files) so
|
|
11
|
+
// it's the authority on what's allowed. We're just trying to set a usable
|
|
12
|
+
// Content-Type for the S3 PUT — and for document files the declared MIME is
|
|
13
|
+
// what routes the post row into the 'file' kind, so it must be accurate.
|
|
11
14
|
const POST_MEDIA_MIME_MAP = {
|
|
12
15
|
".jpg": "image/jpeg",
|
|
13
16
|
".jpeg": "image/jpeg",
|
|
@@ -25,20 +28,30 @@ const POST_MEDIA_MIME_MAP = {
|
|
|
25
28
|
".webm": "video/webm",
|
|
26
29
|
".m4v": "video/x-m4v",
|
|
27
30
|
".pdf": "application/pdf",
|
|
31
|
+
".md": "text/markdown",
|
|
32
|
+
".markdown": "text/markdown",
|
|
33
|
+
".txt": "text/plain",
|
|
34
|
+
".csv": "text/csv",
|
|
28
35
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// the 4
|
|
36
|
+
const FILE_EXTENSIONS = [".pdf", ".md", ".markdown", ".txt", ".csv"];
|
|
37
|
+
const VIDEO_EXTENSIONS = [".mp4", ".mov", ".webm", ".m4v"];
|
|
38
|
+
// Mirrors the backend mix rule: up to 4 photos + up to 4 document files
|
|
39
|
+
// (pdf/md/txt/csv) together, OR 1 GIF, OR 1 video — GIF and video are
|
|
40
|
+
// exclusive with everything. Unknown extensions count against the photo cap
|
|
41
|
+
// (the backend's 'other' kind) so nothing slips through unlimited.
|
|
32
42
|
export function assertPostAttachmentMix(paths) {
|
|
33
43
|
let photos = 0;
|
|
34
44
|
let gifs = 0;
|
|
35
45
|
let videos = 0;
|
|
46
|
+
let files = 0;
|
|
36
47
|
for (const p of paths) {
|
|
37
48
|
const ext = extname(p).toLowerCase();
|
|
38
49
|
if (ext === ".gif")
|
|
39
50
|
gifs += 1;
|
|
40
|
-
else if (
|
|
51
|
+
else if (VIDEO_EXTENSIONS.includes(ext))
|
|
41
52
|
videos += 1;
|
|
53
|
+
else if (FILE_EXTENSIONS.includes(ext))
|
|
54
|
+
files += 1;
|
|
42
55
|
else
|
|
43
56
|
photos += 1;
|
|
44
57
|
}
|
|
@@ -48,10 +61,12 @@ export function assertPostAttachmentMix(paths) {
|
|
|
48
61
|
throw new Error("Only 1 GIF allowed per post");
|
|
49
62
|
if (photos > 4)
|
|
50
63
|
throw new Error("Up to 4 photos allowed per post");
|
|
51
|
-
if (
|
|
64
|
+
if (files > 4)
|
|
65
|
+
throw new Error("Up to 4 files allowed per post");
|
|
66
|
+
if (videos > 0 && (gifs > 0 || photos > 0 || files > 0)) {
|
|
52
67
|
throw new Error("A video can't be combined with other media");
|
|
53
68
|
}
|
|
54
|
-
if (gifs > 0 && (videos > 0 || photos > 0)) {
|
|
69
|
+
if (gifs > 0 && (videos > 0 || photos > 0 || files > 0)) {
|
|
55
70
|
throw new Error("A GIF can't be combined with other media");
|
|
56
71
|
}
|
|
57
72
|
}
|
|
@@ -89,7 +104,15 @@ export async function uploadPostAttachment(filePath) {
|
|
|
89
104
|
if (!putRes.ok) {
|
|
90
105
|
throw new Error(`Failed to PUT ${filePath} to S3: HTTP ${putRes.status}`);
|
|
91
106
|
}
|
|
92
|
-
|
|
107
|
+
const attachment = { mediaUrl, mediaKey };
|
|
108
|
+
// Document files carry name + MIME on the row: the declared mimeType is
|
|
109
|
+
// what the backend trusts for kind/preview routing, and the original
|
|
110
|
+
// filename only survives here (the S3 key is a UUID).
|
|
111
|
+
if (FILE_EXTENSIONS.includes(ext)) {
|
|
112
|
+
attachment.fileName = basename(abs);
|
|
113
|
+
attachment.mimeType = contentType;
|
|
114
|
+
}
|
|
115
|
+
return attachment;
|
|
93
116
|
}
|
|
94
117
|
export async function uploadPostAttachments(paths) {
|
|
95
118
|
const out = [];
|
|
@@ -129,8 +152,9 @@ function addToLocalSyncfiles(gobiDir, filePath) {
|
|
|
129
152
|
if (isPathCovered(filePath, patterns))
|
|
130
153
|
return;
|
|
131
154
|
const syncfilesPath = join(gobiDir, "syncfiles");
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
const pattern = normalizeSyncPattern(filePath);
|
|
156
|
+
appendFileSync(syncfilesPath, `${EOL}${pattern}`);
|
|
157
|
+
console.log(`Added to syncfiles: ${pattern}`);
|
|
134
158
|
}
|
|
135
159
|
export async function uploadAttachments(vaultSlug, links, token, options) {
|
|
136
160
|
const addToSyncfiles = options?.addToSyncfiles ?? false;
|
package/dist/client.js
CHANGED
|
@@ -38,6 +38,9 @@ export function apiPost(path, body) {
|
|
|
38
38
|
export function apiPatch(path, body) {
|
|
39
39
|
return request("PATCH", path, { body });
|
|
40
40
|
}
|
|
41
|
+
export function apiPut(path, body) {
|
|
42
|
+
return request("PUT", path, { body });
|
|
43
|
+
}
|
|
41
44
|
export function apiDelete(path) {
|
|
42
45
|
return request("DELETE", path);
|
|
43
46
|
}
|
package/dist/commands/global.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WEB_BASE_URL } from "../constants.js";
|
|
2
|
-
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
3
|
-
import { isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
|
|
2
|
+
import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
|
|
3
|
+
import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
|
|
4
4
|
import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
|
|
5
5
|
function readContent(value) {
|
|
6
6
|
if (value === "-")
|
|
@@ -26,7 +26,11 @@ function formatFeedLine(m) {
|
|
|
26
26
|
else {
|
|
27
27
|
label = m.title || m.content || "";
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const chips = formatReactionChips(m);
|
|
30
|
+
const attachSummary = formatAttachmentSummary(m);
|
|
31
|
+
return (`${id} ${kind} ${author} "${label}" ${m.createdAt}` +
|
|
32
|
+
(attachSummary ? ` ${attachSummary}` : "") +
|
|
33
|
+
(chips ? ` ${chips}` : ""));
|
|
30
34
|
}
|
|
31
35
|
export function registerGlobalCommand(program) {
|
|
32
36
|
const global = program
|
|
@@ -145,20 +149,28 @@ export function registerGlobalCommand(program) {
|
|
|
145
149
|
`User ${r.authorId}`;
|
|
146
150
|
const text = r.content || "";
|
|
147
151
|
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
|
|
148
|
-
|
|
152
|
+
const rChips = formatReactionChips(r);
|
|
153
|
+
const rAttach = formatAttachmentSummary(r);
|
|
154
|
+
replyLines.push(` - [r:${r.id}] ${rAuthor}: ${truncated} (${r.createdAt})${rAttach ? ` ${rAttach}` : ""}${rChips ? ` ${rChips}` : ""}`);
|
|
149
155
|
}
|
|
150
156
|
const isReplyPost = post.parentPostId != null;
|
|
151
157
|
const heading = isReplyPost
|
|
152
158
|
? `Reply [r:${post.id}]`
|
|
153
159
|
: `Post: ${post.title || "(no title)"}`;
|
|
160
|
+
const postChips = formatReactionChips(post);
|
|
161
|
+
const attachmentLines = formatAttachmentLines(post);
|
|
154
162
|
const output = [
|
|
155
163
|
heading,
|
|
156
164
|
`By: ${author} on ${post.createdAt}`,
|
|
165
|
+
...(postChips ? [`Reactions: ${postChips}`] : []),
|
|
157
166
|
...(ancestorLines.length
|
|
158
167
|
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
159
168
|
: []),
|
|
160
169
|
"",
|
|
161
170
|
post.content || "",
|
|
171
|
+
...(attachmentLines.length
|
|
172
|
+
? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
|
|
173
|
+
: []),
|
|
162
174
|
"",
|
|
163
175
|
`Replies (${replies.length} items):`,
|
|
164
176
|
...replyLines,
|
|
@@ -176,7 +188,7 @@ export function registerGlobalCommand(program) {
|
|
|
176
188
|
.option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
|
|
177
189
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
178
190
|
.option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
179
|
-
.option("--attach <file>", "Local media file to attach. Repeatable.
|
|
191
|
+
.option("--attach <file>", "Local media or document file to attach. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
180
192
|
.option("--repost-post-id <postId>", "Wrap an existing top-level post as the embedded card on this new post. Composes with --content / --rich-text / --attach (the wrapping author's text + media render above the embedded card). Reposts-of-reposts are collapsed to the transitive root server-side. The referenced post must exist, not be deleted, and not itself be a reply.")
|
|
181
193
|
.action(async (opts) => {
|
|
182
194
|
if (!opts.content && !opts.richText) {
|
|
@@ -234,7 +246,7 @@ export function registerGlobalCommand(program) {
|
|
|
234
246
|
.option("--title <title>", "New title")
|
|
235
247
|
.option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
|
|
236
248
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
237
|
-
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable.
|
|
249
|
+
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
|
|
238
250
|
.option("--artifact <artifactId>", "Replace the post's artifact attachments with the given artifact(s) (existing artifact attachments are removed). Repeatable. Omit to leave them unchanged. Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
239
251
|
.action(async (postId, opts) => {
|
|
240
252
|
const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
|
|
@@ -297,7 +309,7 @@ export function registerGlobalCommand(program) {
|
|
|
297
309
|
.description("Create a reply to a post in the global feed.")
|
|
298
310
|
.option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
|
|
299
311
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
300
|
-
.option("--attach <file>", "Local media file to attach to this reply. Repeatable.
|
|
312
|
+
.option("--attach <file>", "Local media or document file to attach to this reply. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
301
313
|
.action(async (postId, opts) => {
|
|
302
314
|
if (!opts.content && !opts.richText) {
|
|
303
315
|
throw new Error("Provide either --content or --rich-text.");
|
|
@@ -376,4 +388,34 @@ export function registerGlobalCommand(program) {
|
|
|
376
388
|
}
|
|
377
389
|
console.log(`Reply ${replyId} deleted.`);
|
|
378
390
|
});
|
|
391
|
+
// ── Reactions (react, unreact) ──
|
|
392
|
+
global
|
|
393
|
+
.command("react <postId> <emoji>")
|
|
394
|
+
.description("Add an emoji reaction to a global-feed post or reply (idempotent). <postId> is the numeric id of a post OR a reply — the [p:N]/[r:N] ids shown in feed output.")
|
|
395
|
+
.action(async (postId, emoji) => {
|
|
396
|
+
const resp = (await apiPut(`/posts/${postId}/reactions`, {
|
|
397
|
+
emoji,
|
|
398
|
+
}));
|
|
399
|
+
const data = unwrapResp(resp);
|
|
400
|
+
if (isJsonMode(global)) {
|
|
401
|
+
jsonOut(data);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const chips = formatReactionChips(data);
|
|
405
|
+
console.log(`Reacted ${emoji} to ${postId}.` + (chips ? `\n Now: ${chips}` : ""));
|
|
406
|
+
});
|
|
407
|
+
global
|
|
408
|
+
.command("unreact <postId> <emoji>")
|
|
409
|
+
.description("Remove your emoji reaction from a global-feed post or reply. <postId> is the numeric id of a post OR a reply.")
|
|
410
|
+
.action(async (postId, emoji) => {
|
|
411
|
+
const resp = (await apiDelete(`/posts/${postId}/reactions/${encodeURIComponent(emoji)}`));
|
|
412
|
+
const data = unwrapResp(resp);
|
|
413
|
+
if (isJsonMode(global)) {
|
|
414
|
+
jsonOut(data);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const chips = formatReactionChips(data);
|
|
418
|
+
console.log(`Removed ${emoji} reaction from ${postId}.` +
|
|
419
|
+
(chips ? `\n Now: ${chips}` : ""));
|
|
420
|
+
});
|
|
379
421
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -31,6 +31,15 @@ export function getVaultSlug() {
|
|
|
31
31
|
}
|
|
32
32
|
return vault;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Starter PUBLISH.md frontmatter. Shared by `vault init` (seed) and
|
|
36
|
+
* `vault publish` (scaffold-on-missing) so the two paths can't drift.
|
|
37
|
+
* `description` is intentionally left blank — the vault won't list publicly
|
|
38
|
+
* until the user fills in both `title` and `description`.
|
|
39
|
+
*/
|
|
40
|
+
export function defaultPublishMd(title) {
|
|
41
|
+
return `---\ntitle: ${title}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`;
|
|
42
|
+
}
|
|
34
43
|
// Per-command requirement markers. Tri-state: true / false override / inherit
|
|
35
44
|
// from parent. The pre-action warning uses these to decide whether to remind
|
|
36
45
|
// the user to run `gobi vault init` / `gobi space warp`.
|
|
@@ -221,7 +230,7 @@ export async function runVaultInitFlow() {
|
|
|
221
230
|
// Create default PUBLISH.md if it doesn't exist
|
|
222
231
|
const publishPath = join(process.cwd(), "PUBLISH.md");
|
|
223
232
|
if (!existsSync(publishPath)) {
|
|
224
|
-
writeFileSync(publishPath,
|
|
233
|
+
writeFileSync(publishPath, defaultPublishMd(vaultName), "utf-8");
|
|
225
234
|
console.log("Created PUBLISH.md");
|
|
226
235
|
}
|
|
227
236
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
2
|
-
import { isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
|
|
1
|
+
import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
|
|
2
|
+
import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
|
|
3
3
|
import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
|
|
4
4
|
function readContent(value) {
|
|
5
5
|
if (value === "-")
|
|
@@ -22,7 +22,11 @@ function formatFeedLine(m) {
|
|
|
22
22
|
else {
|
|
23
23
|
label = m.title || m.content || "";
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
const chips = formatReactionChips(m);
|
|
26
|
+
const attachSummary = formatAttachmentSummary(m);
|
|
27
|
+
return (`${id} ${kind} ${author} "${label}" ${m.createdAt}` +
|
|
28
|
+
(attachSummary ? ` ${attachSummary}` : "") +
|
|
29
|
+
(chips ? ` ${chips}` : ""));
|
|
26
30
|
}
|
|
27
31
|
export function registerPersonalCommand(program) {
|
|
28
32
|
const personal = program
|
|
@@ -147,20 +151,28 @@ export function registerPersonalCommand(program) {
|
|
|
147
151
|
`User ${r.authorId}`;
|
|
148
152
|
const text = r.content || "";
|
|
149
153
|
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
|
|
150
|
-
|
|
154
|
+
const rChips = formatReactionChips(r);
|
|
155
|
+
const rAttach = formatAttachmentSummary(r);
|
|
156
|
+
replyLines.push(` - [r:${r.id}] ${rAuthor}: ${truncated} (${r.createdAt})${rAttach ? ` ${rAttach}` : ""}${rChips ? ` ${rChips}` : ""}`);
|
|
151
157
|
}
|
|
152
158
|
const isReplyPost = post.parentPostId != null;
|
|
153
159
|
const heading = isReplyPost
|
|
154
160
|
? `Reply [r:${post.id}] (private)`
|
|
155
161
|
: `Post: ${post.title || "(no title)"} (private)`;
|
|
162
|
+
const postChips = formatReactionChips(post);
|
|
163
|
+
const attachmentLines = formatAttachmentLines(post);
|
|
156
164
|
const output = [
|
|
157
165
|
heading,
|
|
158
166
|
`By: ${author} on ${post.createdAt}`,
|
|
167
|
+
...(postChips ? [`Reactions: ${postChips}`] : []),
|
|
159
168
|
...(ancestorLines.length
|
|
160
169
|
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
161
170
|
: []),
|
|
162
171
|
"",
|
|
163
172
|
post.content || "",
|
|
173
|
+
...(attachmentLines.length
|
|
174
|
+
? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
|
|
175
|
+
: []),
|
|
164
176
|
"",
|
|
165
177
|
`Replies (${replies.length} items):`,
|
|
166
178
|
...replyLines,
|
|
@@ -184,7 +196,7 @@ export function registerPersonalCommand(program) {
|
|
|
184
196
|
.option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
|
|
185
197
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
186
198
|
.option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
187
|
-
.option("--attach <file>", "Local media file to attach. Repeatable.
|
|
199
|
+
.option("--attach <file>", "Local media or document file to attach. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
188
200
|
.option("--repost-post-id <postId>", "Wrap an existing top-level post as the embedded card on this new private post. The referenced post must be visible to you (your own personal-space post, a global-feed post, or a post in a space you're a member of). Reposting someone else's personal-space post returns 404.")
|
|
189
201
|
.action(async (opts) => {
|
|
190
202
|
if (!opts.content && !opts.richText) {
|
|
@@ -245,7 +257,7 @@ export function registerPersonalCommand(program) {
|
|
|
245
257
|
.option("--title <title>", "New title")
|
|
246
258
|
.option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
|
|
247
259
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
248
|
-
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable.
|
|
260
|
+
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
|
|
249
261
|
.option("--artifact <artifactId>", "Replace the post's artifact attachments with the given artifact(s) (existing artifact attachments are removed). Repeatable. Omit to leave them unchanged. Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
250
262
|
.action(async (postId, opts) => {
|
|
251
263
|
const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
|
|
@@ -313,7 +325,7 @@ export function registerPersonalCommand(program) {
|
|
|
313
325
|
.description("Reply to a personal-space post. The reply inherits the parent's private scope automatically.")
|
|
314
326
|
.option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
|
|
315
327
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
316
|
-
.option("--attach <file>", "Local media file to attach to this reply. Repeatable.
|
|
328
|
+
.option("--attach <file>", "Local media or document file to attach to this reply. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
317
329
|
.action(async (postId, opts) => {
|
|
318
330
|
if (!opts.content && !opts.richText) {
|
|
319
331
|
throw new Error("Provide either --content or --rich-text.");
|
|
@@ -392,4 +404,34 @@ export function registerPersonalCommand(program) {
|
|
|
392
404
|
}
|
|
393
405
|
console.log(`Reply ${replyId} deleted.`);
|
|
394
406
|
});
|
|
407
|
+
// ── Reactions (react, unreact) ──
|
|
408
|
+
personal
|
|
409
|
+
.command("react <postId> <emoji>")
|
|
410
|
+
.description("Add an emoji reaction to a personal-space post or reply (idempotent). <postId> is the numeric id of a post OR a reply.")
|
|
411
|
+
.action(async (postId, emoji) => {
|
|
412
|
+
const resp = (await apiPut(`/posts/${postId}/reactions`, {
|
|
413
|
+
emoji,
|
|
414
|
+
}));
|
|
415
|
+
const data = unwrapResp(resp);
|
|
416
|
+
if (isJsonMode(personal)) {
|
|
417
|
+
jsonOut(data);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const chips = formatReactionChips(data);
|
|
421
|
+
console.log(`Reacted ${emoji} to ${postId}.` + (chips ? `\n Now: ${chips}` : ""));
|
|
422
|
+
});
|
|
423
|
+
personal
|
|
424
|
+
.command("unreact <postId> <emoji>")
|
|
425
|
+
.description("Remove your emoji reaction from a personal-space post or reply. <postId> is the numeric id of a post OR a reply.")
|
|
426
|
+
.action(async (postId, emoji) => {
|
|
427
|
+
const resp = (await apiDelete(`/posts/${postId}/reactions/${encodeURIComponent(emoji)}`));
|
|
428
|
+
const data = unwrapResp(resp);
|
|
429
|
+
if (isJsonMode(personal)) {
|
|
430
|
+
jsonOut(data);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const chips = formatReactionChips(data);
|
|
434
|
+
console.log(`Removed ${emoji} reaction from ${postId}.` +
|
|
435
|
+
(chips ? `\n Now: ${chips}` : ""));
|
|
436
|
+
});
|
|
395
437
|
}
|
package/dist/commands/space.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WEB_BASE_URL } from "../constants.js";
|
|
2
|
-
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
2
|
+
import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
|
|
3
3
|
import { requireSpace, selectSpace, setSpaceRequirement, writeSpaceSetting, } from "./init.js";
|
|
4
|
-
import { isJsonMode, jsonOut, readStdin, resolveSpaceSlug, unwrapResp, } from "./utils.js";
|
|
4
|
+
import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, resolveSpaceSlug, unwrapResp, } from "./utils.js";
|
|
5
5
|
import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
|
|
6
6
|
function readContent(value) {
|
|
7
7
|
if (value === "-")
|
|
@@ -23,7 +23,20 @@ function formatFeedLine(m) {
|
|
|
23
23
|
else {
|
|
24
24
|
label = m.title || m.content || "";
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const chips = formatReactionChips(m);
|
|
27
|
+
const attachSummary = formatAttachmentSummary(m);
|
|
28
|
+
return (`${id} ${kind} ${author} "${label}" ${m.createdAt}` +
|
|
29
|
+
(attachSummary ? ` ${attachSummary}` : "") +
|
|
30
|
+
(chips ? ` ${chips}` : ""));
|
|
31
|
+
}
|
|
32
|
+
function parseChannelIdOption(value) {
|
|
33
|
+
if (value == null)
|
|
34
|
+
return undefined;
|
|
35
|
+
const n = Number(value);
|
|
36
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
37
|
+
throw new Error("--channel must be a positive integer channel id.");
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
27
40
|
}
|
|
28
41
|
export function registerSpaceCommand(program) {
|
|
29
42
|
const space = program
|
|
@@ -179,6 +192,7 @@ export function registerSpaceCommand(program) {
|
|
|
179
192
|
.description("List the unified feed (posts and replies, newest first) in a space.")
|
|
180
193
|
.option("--limit <number>", "Items per page", "20")
|
|
181
194
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
195
|
+
.option("--channel <channelId>", "Channel id to read instead of the main feed (see `list-channels`). Omit for the main feed.")
|
|
182
196
|
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
183
197
|
.action(async (opts) => {
|
|
184
198
|
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
@@ -187,6 +201,7 @@ export function registerSpaceCommand(program) {
|
|
|
187
201
|
};
|
|
188
202
|
if (opts.cursor)
|
|
189
203
|
params.cursor = opts.cursor;
|
|
204
|
+
params.channelId = parseChannelIdOption(opts.channel);
|
|
190
205
|
const resp = (await apiGet(`/spaces/${spaceSlug}/feed`, params));
|
|
191
206
|
if (isJsonMode(space)) {
|
|
192
207
|
jsonOut({
|
|
@@ -251,20 +266,28 @@ export function registerSpaceCommand(program) {
|
|
|
251
266
|
const body = opts.full || !text || text.length <= 200
|
|
252
267
|
? text
|
|
253
268
|
: text.slice(0, 200) + "…";
|
|
254
|
-
|
|
269
|
+
const rChips = formatReactionChips(r);
|
|
270
|
+
const rAttach = formatAttachmentSummary(r);
|
|
271
|
+
replyLines.push(` - [r:${r.id}] ${rAuthor}: ${body} (${r.createdAt})${rAttach ? ` ${rAttach}` : ""}${rChips ? ` ${rChips}` : ""}`);
|
|
255
272
|
}
|
|
256
273
|
const isReplyPost = post.parentPostId != null;
|
|
257
274
|
const heading = isReplyPost
|
|
258
275
|
? `Reply [r:${post.id}]`
|
|
259
276
|
: `Post: ${post.title || "(no title)"}`;
|
|
277
|
+
const postChips = formatReactionChips(post);
|
|
278
|
+
const attachmentLines = formatAttachmentLines(post);
|
|
260
279
|
const output = [
|
|
261
280
|
heading,
|
|
262
281
|
`By: ${author} on ${post.createdAt}`,
|
|
282
|
+
...(postChips ? [`Reactions: ${postChips}`] : []),
|
|
263
283
|
...(ancestorLines.length
|
|
264
284
|
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
265
285
|
: []),
|
|
266
286
|
"",
|
|
267
287
|
post.content || "",
|
|
288
|
+
...(attachmentLines.length
|
|
289
|
+
? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
|
|
290
|
+
: []),
|
|
268
291
|
"",
|
|
269
292
|
`Replies (${replies.length} items):`,
|
|
270
293
|
...replyLines,
|
|
@@ -277,6 +300,7 @@ export function registerSpaceCommand(program) {
|
|
|
277
300
|
.description("List posts in a space (paginated).")
|
|
278
301
|
.option("--limit <number>", "Items per page", "20")
|
|
279
302
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
303
|
+
.option("--channel <channelId>", "Channel id to read instead of the main feed (see `list-channels`). Omit for the main feed.")
|
|
280
304
|
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
281
305
|
.action(async (opts) => {
|
|
282
306
|
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
@@ -285,6 +309,7 @@ export function registerSpaceCommand(program) {
|
|
|
285
309
|
};
|
|
286
310
|
if (opts.cursor)
|
|
287
311
|
params.cursor = opts.cursor;
|
|
312
|
+
params.channelId = parseChannelIdOption(opts.channel);
|
|
288
313
|
const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, params));
|
|
289
314
|
if (isJsonMode(space)) {
|
|
290
315
|
jsonOut({
|
|
@@ -304,7 +329,8 @@ export function registerSpaceCommand(program) {
|
|
|
304
329
|
for (const t of items) {
|
|
305
330
|
const author = t.author?.name ||
|
|
306
331
|
`User ${t.authorId}`;
|
|
307
|
-
|
|
332
|
+
const attachSummary = formatAttachmentSummary(t);
|
|
333
|
+
lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})${attachSummary ? ` ${attachSummary}` : ""}`);
|
|
308
334
|
}
|
|
309
335
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
310
336
|
console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
|
|
@@ -317,8 +343,9 @@ export function registerSpaceCommand(program) {
|
|
|
317
343
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
318
344
|
.option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
319
345
|
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
320
|
-
.option("--attach <file>", "Local media file to attach. Repeatable.
|
|
346
|
+
.option("--attach <file>", "Local media or document file to attach. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
321
347
|
.option("--repost-post-id <postId>", "Wrap an existing top-level post as the embedded card on this new post. Composes with --content / --rich-text / --attach (the wrapping author's text + media render above the embedded card). Reposts-of-reposts are collapsed to the transitive root server-side. The referenced post must exist, not be deleted, and not itself be a reply.")
|
|
348
|
+
.option("--channel <channelId>", "Channel id to post into (see `list-channels`). Omit to post to the space's main feed. You must be able to see the channel (member, space owner/admin, or the space agent on an agent-enabled channel).")
|
|
322
349
|
.action(async (opts) => {
|
|
323
350
|
if (!opts.content && !opts.richText) {
|
|
324
351
|
throw new Error("Provide either --content or --rich-text.");
|
|
@@ -355,6 +382,9 @@ export function registerSpaceCommand(program) {
|
|
|
355
382
|
}
|
|
356
383
|
body.repostPostId = n;
|
|
357
384
|
}
|
|
385
|
+
const channelId = parseChannelIdOption(opts.channel);
|
|
386
|
+
if (channelId != null)
|
|
387
|
+
body.channelId = channelId;
|
|
358
388
|
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
359
389
|
const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, body));
|
|
360
390
|
const post = unwrapResp(resp);
|
|
@@ -376,7 +406,7 @@ export function registerSpaceCommand(program) {
|
|
|
376
406
|
.option("--content <content>", "New content for the post (markdown supported, use \"-\" for stdin)")
|
|
377
407
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
378
408
|
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
379
|
-
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable.
|
|
409
|
+
.option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
|
|
380
410
|
.option("--artifact <artifactId>", "Replace the post's artifact attachments with the given artifact(s) (existing artifact attachments are removed). Repeatable. Omit to leave them unchanged. Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
|
|
381
411
|
.action(async (postId, opts) => {
|
|
382
412
|
const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
|
|
@@ -445,7 +475,7 @@ export function registerSpaceCommand(program) {
|
|
|
445
475
|
.option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
|
|
446
476
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
447
477
|
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
448
|
-
.option("--attach <file>", "Local media file to attach to this reply. Repeatable.
|
|
478
|
+
.option("--attach <file>", "Local media or document file to attach to this reply. Repeatable. Mix rule: up to 4 photos + up to 4 document files (pdf/md/txt/csv) OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files.", (value, prev = []) => [...prev, value], [])
|
|
449
479
|
.action(async (postId, opts) => {
|
|
450
480
|
if (!opts.content && !opts.richText) {
|
|
451
481
|
throw new Error("Provide either --content or --rich-text.");
|
|
@@ -530,4 +560,113 @@ export function registerSpaceCommand(program) {
|
|
|
530
560
|
}
|
|
531
561
|
console.log(`Reply ${replyId} deleted.`);
|
|
532
562
|
});
|
|
563
|
+
// ── Reactions (react, unreact) ──
|
|
564
|
+
space
|
|
565
|
+
.command("react <postId> <emoji>")
|
|
566
|
+
.description("Add an emoji reaction to a post or reply (idempotent). <postId> is the numeric id of a post OR a reply — the [p:N]/[r:N] ids shown in feed output.")
|
|
567
|
+
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
568
|
+
.action(async (postId, emoji, opts) => {
|
|
569
|
+
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
570
|
+
const resp = (await apiPut(`/spaces/${spaceSlug}/posts/${postId}/reactions`, { emoji }));
|
|
571
|
+
const data = unwrapResp(resp);
|
|
572
|
+
if (isJsonMode(space)) {
|
|
573
|
+
jsonOut(data);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const chips = formatReactionChips(data);
|
|
577
|
+
console.log(`Reacted ${emoji} to ${postId}.` + (chips ? `\n Now: ${chips}` : ""));
|
|
578
|
+
});
|
|
579
|
+
space
|
|
580
|
+
.command("unreact <postId> <emoji>")
|
|
581
|
+
.description("Remove your emoji reaction from a post or reply. <postId> is the numeric id of a post OR a reply.")
|
|
582
|
+
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
583
|
+
.action(async (postId, emoji, opts) => {
|
|
584
|
+
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
585
|
+
const resp = (await apiDelete(`/spaces/${spaceSlug}/posts/${postId}/reactions/${encodeURIComponent(emoji)}`));
|
|
586
|
+
const data = unwrapResp(resp);
|
|
587
|
+
if (isJsonMode(space)) {
|
|
588
|
+
jsonOut(data);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const chips = formatReactionChips(data);
|
|
592
|
+
console.log(`Removed ${emoji} reaction from ${postId}.` +
|
|
593
|
+
(chips ? `\n Now: ${chips}` : ""));
|
|
594
|
+
});
|
|
595
|
+
// ── Channels (read-only; channel admin is web-UI only) ──
|
|
596
|
+
//
|
|
597
|
+
// Private member-gated sub-feeds inside a space. The main feed is virtual
|
|
598
|
+
// (no channel row): feed/list-posts/create-post without --channel target
|
|
599
|
+
// it. Members see their channels; space owners/admins see all; the space
|
|
600
|
+
// agent sees agent-enabled channels only. Create/rename/delete and roster
|
|
601
|
+
// management are deliberately not exposed here — like space and member
|
|
602
|
+
// admin, that's web-UI territory.
|
|
603
|
+
space
|
|
604
|
+
.command("list-channels")
|
|
605
|
+
.description("List channels visible to you in a space (members: yours; space owner/admin: all). The main feed is not a channel — read it by omitting --channel on `feed`.")
|
|
606
|
+
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
607
|
+
.action(async (opts) => {
|
|
608
|
+
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
609
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/channels`));
|
|
610
|
+
const items = (resp.data || []);
|
|
611
|
+
if (isJsonMode(space)) {
|
|
612
|
+
jsonOut(items);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!items.length) {
|
|
616
|
+
console.log("No channels found.");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const lines = [];
|
|
620
|
+
for (const c of items) {
|
|
621
|
+
const flags = [
|
|
622
|
+
`${c.memberCount} member${c.memberCount === 1 ? "" : "s"}`,
|
|
623
|
+
c.isMember ? "member: you" : "member: no",
|
|
624
|
+
c.agentAccess ? "agent: on" : "agent: off",
|
|
625
|
+
].join(", ");
|
|
626
|
+
lines.push(`- [${c.id}] #${c.name} (${flags})`);
|
|
627
|
+
if (c.description)
|
|
628
|
+
lines.push(` Description: ${c.description}`);
|
|
629
|
+
}
|
|
630
|
+
console.log(`Channels (${items.length}):\n` + lines.join("\n"));
|
|
631
|
+
});
|
|
632
|
+
space
|
|
633
|
+
.command("get-channel <channelId>")
|
|
634
|
+
.description("Get one channel (channel members, space owner/admin, or the agent on agent-enabled channels).")
|
|
635
|
+
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
636
|
+
.action(async (channelId, opts) => {
|
|
637
|
+
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
638
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/channels/${channelId}`));
|
|
639
|
+
const c = unwrapResp(resp);
|
|
640
|
+
if (isJsonMode(space)) {
|
|
641
|
+
jsonOut(c);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const desc = c.description ? `\n Description: ${c.description}` : "";
|
|
645
|
+
console.log(`Channel [${c.id}] #${c.name}${desc}\n` +
|
|
646
|
+
` Agent access: ${c.agentAccess ? "on" : "off"}\n` +
|
|
647
|
+
` Created: ${c.createdAt}`);
|
|
648
|
+
});
|
|
649
|
+
space
|
|
650
|
+
.command("list-channel-members <channelId>")
|
|
651
|
+
.description("List the members of a channel.")
|
|
652
|
+
.option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
|
|
653
|
+
.action(async (channelId, opts) => {
|
|
654
|
+
const spaceSlug = resolveSpaceSlug(space, opts);
|
|
655
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/channels/${channelId}/members`));
|
|
656
|
+
const items = (resp.data || []);
|
|
657
|
+
if (isJsonMode(space)) {
|
|
658
|
+
jsonOut(items);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!items.length) {
|
|
662
|
+
console.log("No members found.");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const lines = [];
|
|
666
|
+
for (const m of items) {
|
|
667
|
+
const user = (m.user || {});
|
|
668
|
+
lines.push(`- [${m.userId}] ${user.name || "Unknown"} (joined ${m.createdAt})`);
|
|
669
|
+
}
|
|
670
|
+
console.log(`Channel members (${items.length}):\n` + lines.join("\n"));
|
|
671
|
+
});
|
|
533
672
|
}
|