@gobi-ai/cli 2.0.30 → 2.0.32
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 +31 -9
- package/dist/client.js +3 -0
- package/dist/commands/global.js +49 -7
- package/dist/commands/personal.js +83 -7
- package/dist/commands/space.js +185 -8
- package/dist/commands/utils.js +70 -0
- 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 +1 -1
- package/skills/gobi-media/SKILL.md +2 -2
- package/skills/gobi-sense/SKILL.md +2 -2
- package/skills/gobi-space/SKILL.md +32 -8
- package/skills/gobi-space/references/global.md +30 -5
- package/skills/gobi-space/references/personal.md +46 -5
- package/skills/gobi-space/references/space.md +109 -20
- package/skills/gobi-vault/SKILL.md +2 -2
|
@@ -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.32",
|
|
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.32",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/attachments.js
CHANGED
|
@@ -7,8 +7,10 @@ import { apiPost } from "./client.js";
|
|
|
7
7
|
import { normalizeSyncPattern } from "./commands/sync.js";
|
|
8
8
|
// Best-effort extension → MIME mapping. Anything we don't recognize falls
|
|
9
9
|
// back to `application/octet-stream`; the backend caps size per content-type
|
|
10
|
-
// tier (
|
|
11
|
-
// 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.
|
|
12
14
|
const POST_MEDIA_MIME_MAP = {
|
|
13
15
|
".jpg": "image/jpeg",
|
|
14
16
|
".jpeg": "image/jpeg",
|
|
@@ -26,20 +28,30 @@ const POST_MEDIA_MIME_MAP = {
|
|
|
26
28
|
".webm": "video/webm",
|
|
27
29
|
".m4v": "video/x-m4v",
|
|
28
30
|
".pdf": "application/pdf",
|
|
31
|
+
".md": "text/markdown",
|
|
32
|
+
".markdown": "text/markdown",
|
|
33
|
+
".txt": "text/plain",
|
|
34
|
+
".csv": "text/csv",
|
|
29
35
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// 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.
|
|
33
42
|
export function assertPostAttachmentMix(paths) {
|
|
34
43
|
let photos = 0;
|
|
35
44
|
let gifs = 0;
|
|
36
45
|
let videos = 0;
|
|
46
|
+
let files = 0;
|
|
37
47
|
for (const p of paths) {
|
|
38
48
|
const ext = extname(p).toLowerCase();
|
|
39
49
|
if (ext === ".gif")
|
|
40
50
|
gifs += 1;
|
|
41
|
-
else if (
|
|
51
|
+
else if (VIDEO_EXTENSIONS.includes(ext))
|
|
42
52
|
videos += 1;
|
|
53
|
+
else if (FILE_EXTENSIONS.includes(ext))
|
|
54
|
+
files += 1;
|
|
43
55
|
else
|
|
44
56
|
photos += 1;
|
|
45
57
|
}
|
|
@@ -49,10 +61,12 @@ export function assertPostAttachmentMix(paths) {
|
|
|
49
61
|
throw new Error("Only 1 GIF allowed per post");
|
|
50
62
|
if (photos > 4)
|
|
51
63
|
throw new Error("Up to 4 photos allowed per post");
|
|
52
|
-
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)) {
|
|
53
67
|
throw new Error("A video can't be combined with other media");
|
|
54
68
|
}
|
|
55
|
-
if (gifs > 0 && (videos > 0 || photos > 0)) {
|
|
69
|
+
if (gifs > 0 && (videos > 0 || photos > 0 || files > 0)) {
|
|
56
70
|
throw new Error("A GIF can't be combined with other media");
|
|
57
71
|
}
|
|
58
72
|
}
|
|
@@ -90,7 +104,15 @@ export async function uploadPostAttachment(filePath) {
|
|
|
90
104
|
if (!putRes.ok) {
|
|
91
105
|
throw new Error(`Failed to PUT ${filePath} to S3: HTTP ${putRes.status}`);
|
|
92
106
|
}
|
|
93
|
-
|
|
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;
|
|
94
116
|
}
|
|
95
117
|
export async function uploadPostAttachments(paths) {
|
|
96
118
|
const out = [];
|
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
|
}
|
|
@@ -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
|
|
@@ -62,6 +66,40 @@ export function registerPersonalCommand(program) {
|
|
|
62
66
|
lines.join("\n") +
|
|
63
67
|
footer);
|
|
64
68
|
});
|
|
69
|
+
// ── Search ──
|
|
70
|
+
personal
|
|
71
|
+
.command("search-posts <query>")
|
|
72
|
+
.description("Search your personal-space posts and replies (newest first). The query supports keywords " +
|
|
73
|
+
"plus from:<name> and topic:<tag> operators (quote multi-word values). " +
|
|
74
|
+
"Each result is an individual post or reply, not a whole thread.")
|
|
75
|
+
.option("--limit <number>", "Items per page", "20")
|
|
76
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
77
|
+
.action(async (query, opts) => {
|
|
78
|
+
const params = {
|
|
79
|
+
q: query,
|
|
80
|
+
limit: parseInt(opts.limit, 10),
|
|
81
|
+
};
|
|
82
|
+
if (opts.cursor)
|
|
83
|
+
params.cursor = opts.cursor;
|
|
84
|
+
const resp = (await apiGet(`/posts/personal-space/search`, params));
|
|
85
|
+
if (isJsonMode(personal)) {
|
|
86
|
+
jsonOut({
|
|
87
|
+
items: resp.data || [],
|
|
88
|
+
pagination: resp.pagination || {},
|
|
89
|
+
mentions: resp.mentions || {},
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const items = (resp.data || []);
|
|
94
|
+
const pagination = (resp.pagination || {});
|
|
95
|
+
if (!items.length) {
|
|
96
|
+
console.log("No results found.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const lines = items.map(formatFeedLine);
|
|
100
|
+
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
101
|
+
console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
102
|
+
});
|
|
65
103
|
// ── List posts ──
|
|
66
104
|
//
|
|
67
105
|
// No server-side roots-only endpoint exists for the personal-space lane;
|
|
@@ -147,20 +185,28 @@ export function registerPersonalCommand(program) {
|
|
|
147
185
|
`User ${r.authorId}`;
|
|
148
186
|
const text = r.content || "";
|
|
149
187
|
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
|
|
150
|
-
|
|
188
|
+
const rChips = formatReactionChips(r);
|
|
189
|
+
const rAttach = formatAttachmentSummary(r);
|
|
190
|
+
replyLines.push(` - [r:${r.id}] ${rAuthor}: ${truncated} (${r.createdAt})${rAttach ? ` ${rAttach}` : ""}${rChips ? ` ${rChips}` : ""}`);
|
|
151
191
|
}
|
|
152
192
|
const isReplyPost = post.parentPostId != null;
|
|
153
193
|
const heading = isReplyPost
|
|
154
194
|
? `Reply [r:${post.id}] (private)`
|
|
155
195
|
: `Post: ${post.title || "(no title)"} (private)`;
|
|
196
|
+
const postChips = formatReactionChips(post);
|
|
197
|
+
const attachmentLines = formatAttachmentLines(post);
|
|
156
198
|
const output = [
|
|
157
199
|
heading,
|
|
158
200
|
`By: ${author} on ${post.createdAt}`,
|
|
201
|
+
...(postChips ? [`Reactions: ${postChips}`] : []),
|
|
159
202
|
...(ancestorLines.length
|
|
160
203
|
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
161
204
|
: []),
|
|
162
205
|
"",
|
|
163
206
|
post.content || "",
|
|
207
|
+
...(attachmentLines.length
|
|
208
|
+
? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
|
|
209
|
+
: []),
|
|
164
210
|
"",
|
|
165
211
|
`Replies (${replies.length} items):`,
|
|
166
212
|
...replyLines,
|
|
@@ -184,7 +230,7 @@ export function registerPersonalCommand(program) {
|
|
|
184
230
|
.option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
|
|
185
231
|
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
186
232
|
.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.
|
|
233
|
+
.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
234
|
.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
235
|
.action(async (opts) => {
|
|
190
236
|
if (!opts.content && !opts.richText) {
|
|
@@ -245,7 +291,7 @@ export function registerPersonalCommand(program) {
|
|
|
245
291
|
.option("--title <title>", "New title")
|
|
246
292
|
.option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
|
|
247
293
|
.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.
|
|
294
|
+
.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
295
|
.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
296
|
.action(async (postId, opts) => {
|
|
251
297
|
const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
|
|
@@ -313,7 +359,7 @@ export function registerPersonalCommand(program) {
|
|
|
313
359
|
.description("Reply to a personal-space post. The reply inherits the parent's private scope automatically.")
|
|
314
360
|
.option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
|
|
315
361
|
.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.
|
|
362
|
+
.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
363
|
.action(async (postId, opts) => {
|
|
318
364
|
if (!opts.content && !opts.richText) {
|
|
319
365
|
throw new Error("Provide either --content or --rich-text.");
|
|
@@ -392,4 +438,34 @@ export function registerPersonalCommand(program) {
|
|
|
392
438
|
}
|
|
393
439
|
console.log(`Reply ${replyId} deleted.`);
|
|
394
440
|
});
|
|
441
|
+
// ── Reactions (react, unreact) ──
|
|
442
|
+
personal
|
|
443
|
+
.command("react <postId> <emoji>")
|
|
444
|
+
.description("Add an emoji reaction to a personal-space post or reply (idempotent). <postId> is the numeric id of a post OR a reply.")
|
|
445
|
+
.action(async (postId, emoji) => {
|
|
446
|
+
const resp = (await apiPut(`/posts/${postId}/reactions`, {
|
|
447
|
+
emoji,
|
|
448
|
+
}));
|
|
449
|
+
const data = unwrapResp(resp);
|
|
450
|
+
if (isJsonMode(personal)) {
|
|
451
|
+
jsonOut(data);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const chips = formatReactionChips(data);
|
|
455
|
+
console.log(`Reacted ${emoji} to ${postId}.` + (chips ? `\n Now: ${chips}` : ""));
|
|
456
|
+
});
|
|
457
|
+
personal
|
|
458
|
+
.command("unreact <postId> <emoji>")
|
|
459
|
+
.description("Remove your emoji reaction from a personal-space post or reply. <postId> is the numeric id of a post OR a reply.")
|
|
460
|
+
.action(async (postId, emoji) => {
|
|
461
|
+
const resp = (await apiDelete(`/posts/${postId}/reactions/${encodeURIComponent(emoji)}`));
|
|
462
|
+
const data = unwrapResp(resp);
|
|
463
|
+
if (isJsonMode(personal)) {
|
|
464
|
+
jsonOut(data);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const chips = formatReactionChips(data);
|
|
468
|
+
console.log(`Removed ${emoji} reaction from ${postId}.` +
|
|
469
|
+
(chips ? `\n Now: ${chips}` : ""));
|
|
470
|
+
});
|
|
395
471
|
}
|