@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.
@@ -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
- return `${id} ${kind} ${author} "${label}" ${m.createdAt}`;
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({
@@ -205,6 +220,44 @@ export function registerSpaceCommand(program) {
205
220
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
206
221
  console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
207
222
  });
223
+ // ── Search ──
224
+ space
225
+ .command("search-posts <query>")
226
+ .description("Search a space's posts and replies (newest first). The query supports keywords " +
227
+ 'plus from:<name> and topic:<tag> operators (quote multi-word values, e.g. from:"Jane Doe"). ' +
228
+ "Each result is an individual post or reply, not a whole thread.")
229
+ .option("--limit <number>", "Items per page", "20")
230
+ .option("--cursor <string>", "Pagination cursor from previous response")
231
+ .option("--channel <channelId>", "Restrict results to one channel (see `list-channels`). Omit to search the main feed and all channels visible to you.")
232
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
233
+ .action(async (query, opts) => {
234
+ const spaceSlug = resolveSpaceSlug(space, opts);
235
+ const params = {
236
+ q: query,
237
+ limit: parseInt(opts.limit, 10),
238
+ };
239
+ if (opts.cursor)
240
+ params.cursor = opts.cursor;
241
+ params.channelId = parseChannelIdOption(opts.channel);
242
+ const resp = (await apiGet(`/spaces/${spaceSlug}/search`, params));
243
+ if (isJsonMode(space)) {
244
+ jsonOut({
245
+ items: resp.data || [],
246
+ pagination: resp.pagination || {},
247
+ mentions: resp.mentions || {},
248
+ });
249
+ return;
250
+ }
251
+ const items = (resp.data || []);
252
+ const pagination = (resp.pagination || {});
253
+ if (!items.length) {
254
+ console.log("No results found.");
255
+ return;
256
+ }
257
+ const lines = items.map(formatFeedLine);
258
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
259
+ console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
260
+ });
208
261
  // ── Posts (get, list, create, edit, delete) ──
209
262
  space
210
263
  .command("get-post <postId>")
@@ -251,20 +304,28 @@ export function registerSpaceCommand(program) {
251
304
  const body = opts.full || !text || text.length <= 200
252
305
  ? text
253
306
  : text.slice(0, 200) + "…";
254
- replyLines.push(` - ${rAuthor}: ${body} (${r.createdAt})`);
307
+ const rChips = formatReactionChips(r);
308
+ const rAttach = formatAttachmentSummary(r);
309
+ replyLines.push(` - [r:${r.id}] ${rAuthor}: ${body} (${r.createdAt})${rAttach ? ` ${rAttach}` : ""}${rChips ? ` ${rChips}` : ""}`);
255
310
  }
256
311
  const isReplyPost = post.parentPostId != null;
257
312
  const heading = isReplyPost
258
313
  ? `Reply [r:${post.id}]`
259
314
  : `Post: ${post.title || "(no title)"}`;
315
+ const postChips = formatReactionChips(post);
316
+ const attachmentLines = formatAttachmentLines(post);
260
317
  const output = [
261
318
  heading,
262
319
  `By: ${author} on ${post.createdAt}`,
320
+ ...(postChips ? [`Reactions: ${postChips}`] : []),
263
321
  ...(ancestorLines.length
264
322
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
265
323
  : []),
266
324
  "",
267
325
  post.content || "",
326
+ ...(attachmentLines.length
327
+ ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
328
+ : []),
268
329
  "",
269
330
  `Replies (${replies.length} items):`,
270
331
  ...replyLines,
@@ -277,6 +338,7 @@ export function registerSpaceCommand(program) {
277
338
  .description("List posts in a space (paginated).")
278
339
  .option("--limit <number>", "Items per page", "20")
279
340
  .option("--cursor <string>", "Pagination cursor from previous response")
341
+ .option("--channel <channelId>", "Channel id to read instead of the main feed (see `list-channels`). Omit for the main feed.")
280
342
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
281
343
  .action(async (opts) => {
282
344
  const spaceSlug = resolveSpaceSlug(space, opts);
@@ -285,6 +347,7 @@ export function registerSpaceCommand(program) {
285
347
  };
286
348
  if (opts.cursor)
287
349
  params.cursor = opts.cursor;
350
+ params.channelId = parseChannelIdOption(opts.channel);
288
351
  const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, params));
289
352
  if (isJsonMode(space)) {
290
353
  jsonOut({
@@ -304,7 +367,8 @@ export function registerSpaceCommand(program) {
304
367
  for (const t of items) {
305
368
  const author = t.author?.name ||
306
369
  `User ${t.authorId}`;
307
- lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
370
+ const attachSummary = formatAttachmentSummary(t);
371
+ lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})${attachSummary ? ` ${attachSummary}` : ""}`);
308
372
  }
309
373
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
310
374
  console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
@@ -317,8 +381,9 @@ export function registerSpaceCommand(program) {
317
381
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
318
382
  .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
319
383
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
320
- .option("--attach <file>", "Local media file to attach. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
384
+ .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
385
  .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.")
386
+ .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
387
  .action(async (opts) => {
323
388
  if (!opts.content && !opts.richText) {
324
389
  throw new Error("Provide either --content or --rich-text.");
@@ -355,6 +420,9 @@ export function registerSpaceCommand(program) {
355
420
  }
356
421
  body.repostPostId = n;
357
422
  }
423
+ const channelId = parseChannelIdOption(opts.channel);
424
+ if (channelId != null)
425
+ body.channelId = channelId;
358
426
  const spaceSlug = resolveSpaceSlug(space, opts);
359
427
  const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, body));
360
428
  const post = unwrapResp(resp);
@@ -376,7 +444,7 @@ export function registerSpaceCommand(program) {
376
444
  .option("--content <content>", "New content for the post (markdown supported, use \"-\" for stdin)")
377
445
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
378
446
  .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. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
447
+ .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
448
  .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
449
  .action(async (postId, opts) => {
382
450
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
@@ -445,7 +513,7 @@ export function registerSpaceCommand(program) {
445
513
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
446
514
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
447
515
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
448
- .option("--attach <file>", "Local media file to attach to this reply. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
516
+ .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
517
  .action(async (postId, opts) => {
450
518
  if (!opts.content && !opts.richText) {
451
519
  throw new Error("Provide either --content or --rich-text.");
@@ -530,4 +598,113 @@ export function registerSpaceCommand(program) {
530
598
  }
531
599
  console.log(`Reply ${replyId} deleted.`);
532
600
  });
601
+ // ── Reactions (react, unreact) ──
602
+ space
603
+ .command("react <postId> <emoji>")
604
+ .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.")
605
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
606
+ .action(async (postId, emoji, opts) => {
607
+ const spaceSlug = resolveSpaceSlug(space, opts);
608
+ const resp = (await apiPut(`/spaces/${spaceSlug}/posts/${postId}/reactions`, { emoji }));
609
+ const data = unwrapResp(resp);
610
+ if (isJsonMode(space)) {
611
+ jsonOut(data);
612
+ return;
613
+ }
614
+ const chips = formatReactionChips(data);
615
+ console.log(`Reacted ${emoji} to ${postId}.` + (chips ? `\n Now: ${chips}` : ""));
616
+ });
617
+ space
618
+ .command("unreact <postId> <emoji>")
619
+ .description("Remove your emoji reaction from a post or reply. <postId> is the numeric id of a post OR a reply.")
620
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
621
+ .action(async (postId, emoji, opts) => {
622
+ const spaceSlug = resolveSpaceSlug(space, opts);
623
+ const resp = (await apiDelete(`/spaces/${spaceSlug}/posts/${postId}/reactions/${encodeURIComponent(emoji)}`));
624
+ const data = unwrapResp(resp);
625
+ if (isJsonMode(space)) {
626
+ jsonOut(data);
627
+ return;
628
+ }
629
+ const chips = formatReactionChips(data);
630
+ console.log(`Removed ${emoji} reaction from ${postId}.` +
631
+ (chips ? `\n Now: ${chips}` : ""));
632
+ });
633
+ // ── Channels (read-only; channel admin is web-UI only) ──
634
+ //
635
+ // Private member-gated sub-feeds inside a space. The main feed is virtual
636
+ // (no channel row): feed/list-posts/create-post without --channel target
637
+ // it. Members see their channels; space owners/admins see all; the space
638
+ // agent sees agent-enabled channels only. Create/rename/delete and roster
639
+ // management are deliberately not exposed here — like space and member
640
+ // admin, that's web-UI territory.
641
+ space
642
+ .command("list-channels")
643
+ .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`.")
644
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
645
+ .action(async (opts) => {
646
+ const spaceSlug = resolveSpaceSlug(space, opts);
647
+ const resp = (await apiGet(`/spaces/${spaceSlug}/channels`));
648
+ const items = (resp.data || []);
649
+ if (isJsonMode(space)) {
650
+ jsonOut(items);
651
+ return;
652
+ }
653
+ if (!items.length) {
654
+ console.log("No channels found.");
655
+ return;
656
+ }
657
+ const lines = [];
658
+ for (const c of items) {
659
+ const flags = [
660
+ `${c.memberCount} member${c.memberCount === 1 ? "" : "s"}`,
661
+ c.isMember ? "member: you" : "member: no",
662
+ c.agentAccess ? "agent: on" : "agent: off",
663
+ ].join(", ");
664
+ lines.push(`- [${c.id}] #${c.name} (${flags})`);
665
+ if (c.description)
666
+ lines.push(` Description: ${c.description}`);
667
+ }
668
+ console.log(`Channels (${items.length}):\n` + lines.join("\n"));
669
+ });
670
+ space
671
+ .command("get-channel <channelId>")
672
+ .description("Get one channel (channel members, space owner/admin, or the agent on agent-enabled channels).")
673
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
674
+ .action(async (channelId, opts) => {
675
+ const spaceSlug = resolveSpaceSlug(space, opts);
676
+ const resp = (await apiGet(`/spaces/${spaceSlug}/channels/${channelId}`));
677
+ const c = unwrapResp(resp);
678
+ if (isJsonMode(space)) {
679
+ jsonOut(c);
680
+ return;
681
+ }
682
+ const desc = c.description ? `\n Description: ${c.description}` : "";
683
+ console.log(`Channel [${c.id}] #${c.name}${desc}\n` +
684
+ ` Agent access: ${c.agentAccess ? "on" : "off"}\n` +
685
+ ` Created: ${c.createdAt}`);
686
+ });
687
+ space
688
+ .command("list-channel-members <channelId>")
689
+ .description("List the members of a channel.")
690
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
691
+ .action(async (channelId, opts) => {
692
+ const spaceSlug = resolveSpaceSlug(space, opts);
693
+ const resp = (await apiGet(`/spaces/${spaceSlug}/channels/${channelId}/members`));
694
+ const items = (resp.data || []);
695
+ if (isJsonMode(space)) {
696
+ jsonOut(items);
697
+ return;
698
+ }
699
+ if (!items.length) {
700
+ console.log("No members found.");
701
+ return;
702
+ }
703
+ const lines = [];
704
+ for (const m of items) {
705
+ const user = (m.user || {});
706
+ lines.push(`- [${m.userId}] ${user.name || "Unknown"} (joined ${m.createdAt})`);
707
+ }
708
+ console.log(`Channel members (${items.length}):\n` + lines.join("\n"));
709
+ });
533
710
  }
@@ -28,3 +28,73 @@ export function unwrapResp(resp) {
28
28
  }
29
29
  return resp;
30
30
  }
31
+ // Compact reaction chips for single-line output: `👍2* 🎉1` — the trailing
32
+ // `*` marks emojis the caller has reacted with (remove via `unreact`).
33
+ export function formatReactionChips(m) {
34
+ const reactions = m.reactions || [];
35
+ if (!Array.isArray(reactions) || !reactions.length)
36
+ return "";
37
+ return reactions
38
+ .map((r) => `${r.emoji}${r.count}${r.reactedByMe ? "*" : ""}`)
39
+ .join(" ");
40
+ }
41
+ // Classify a wire attachment the way the clients do: declared mimeType first,
42
+ // then the media-url extension (covers rows that predate the mimeType field).
43
+ function attachmentKind(a) {
44
+ if (a.artifact)
45
+ return "artifact";
46
+ const mime = (a.mimeType || "").toLowerCase();
47
+ const url = (a.mediaUrl || "").toLowerCase().split("?")[0];
48
+ if (mime === "image/gif" || (!mime && url.endsWith(".gif")))
49
+ return "gif";
50
+ if (mime.startsWith("image/"))
51
+ return "photo";
52
+ if (mime.startsWith("video/"))
53
+ return "video";
54
+ if (mime)
55
+ return "file";
56
+ if (/\.(jpe?g|png|webp|heic|heif|avif|svg|bmp|tiff)$/.test(url))
57
+ return "photo";
58
+ if (/\.(mp4|mov|webm|m4v)$/.test(url))
59
+ return "video";
60
+ if (/\.(pdf|md|txt|csv)$/.test(url))
61
+ return "file";
62
+ return "media";
63
+ }
64
+ // Compact attachment marker for single-line output: `📎 2 photos, 1 file`.
65
+ // Counts per kind; full URLs are in `get-post` (or --json).
66
+ export function formatAttachmentSummary(m) {
67
+ const attachments = m.attachments || [];
68
+ if (!Array.isArray(attachments) || !attachments.length)
69
+ return "";
70
+ const counts = new Map();
71
+ for (const a of attachments) {
72
+ const kind = attachmentKind(a);
73
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
74
+ }
75
+ const parts = [];
76
+ for (const [kind, count] of counts) {
77
+ parts.push(count === 1 ? `1 ${kind}` : `${count} ${kind}s`);
78
+ }
79
+ return `📎 ${parts.join(", ")}`;
80
+ }
81
+ // One line per attachment with the fetchable URL — used by `get-post` so an
82
+ // agent can download/describe the media without re-querying in --json mode.
83
+ export function formatAttachmentLines(m, indent = " ") {
84
+ const attachments = m.attachments || [];
85
+ if (!Array.isArray(attachments) || !attachments.length)
86
+ return [];
87
+ return attachments.map((a) => {
88
+ const kind = attachmentKind(a);
89
+ if (kind === "artifact") {
90
+ const art = a.artifact;
91
+ const title = art.title ? ` "${art.title}"` : "";
92
+ const status = art.isPublished ? "published" : "unpublished";
93
+ return `${indent}- artifact [${art.kind}]${title} (${art.artifactId}, ${status})${art.mediaUrl ? ` — ${art.mediaUrl}` : ""}`;
94
+ }
95
+ const dims = a.width && a.height ? ` ${a.width}×${a.height}` : "";
96
+ const name = a.fileName ? ` "${a.fileName}"` : "";
97
+ const mime = a.mimeType ? ` (${a.mimeType})` : "";
98
+ return `${indent}- ${kind}${name}${dims}${mime} — ${a.mediaUrl}`;
99
+ });
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,12 +9,12 @@ description: >-
9
9
  allowed-tools: Bash(gobi:*)
10
10
  metadata:
11
11
  author: gobi-ai
12
- version: "2.0.30"
12
+ version: "2.0.32"
13
13
  ---
14
14
 
15
15
  # gobi-artifact
16
16
 
17
- Gobi artifact commands for versioned, post-attachable creations (v2.0.30).
17
+ Gobi artifact commands for versioned, post-attachable creations (v2.0.32).
18
18
 
19
19
  Requires gobi-cli installed and authenticated. See gobi-core skill for setup.
20
20
 
@@ -8,12 +8,12 @@ description: >-
8
8
  allowed-tools: Bash(gobi:*)
9
9
  metadata:
10
10
  author: gobi-ai
11
- version: "2.0.30"
11
+ version: "2.0.32"
12
12
  ---
13
13
 
14
14
  # gobi-core
15
15
 
16
- Core CLI commands for the Gobi collaborative knowledge platform (v2.0.30).
16
+ Core CLI commands for the Gobi collaborative knowledge platform (v2.0.32).
17
17
 
18
18
  ## Prerequisites
19
19
 
@@ -6,13 +6,13 @@ Usage: gobi space [options] [command]
6
6
  Space commands (posts, replies). Space and member admin is web-UI only.
7
7
 
8
8
  Options:
9
- --space-slug <spaceSlug> Space slug (overrides .gobi/settings.yaml)
10
- -h, --help display help for command
9
+ --space-slug <spaceSlug> Space slug (overrides .gobi/settings.yaml)
10
+ -h, --help display help for command
11
11
 
12
12
  Commands:
13
- list List spaces you are a member of.
14
- warp [spaceSlug] Select the active space. Pass a slug to warp directly, or omit for interactive selection.
15
- help [command] display help for command
13
+ list List spaces you are a member of.
14
+ warp [spaceSlug] Select the active space. Pass a slug to warp directly, or omit for interactive selection.
15
+ help [command] display help for command
16
16
  ```
17
17
 
18
18
  ## list
@@ -7,7 +7,7 @@ description: >-
7
7
  allowed-tools: Bash(gobi:*)
8
8
  metadata:
9
9
  author: gobi-ai
10
- version: "2.0.30"
10
+ version: "2.0.32"
11
11
  ---
12
12
 
13
13
  # Gobi Homepage Developer Guide
@@ -10,12 +10,12 @@ description: >-
10
10
  allowed-tools: Bash(gobi:*)
11
11
  metadata:
12
12
  author: gobi-ai
13
- version: "2.0.30"
13
+ version: "2.0.32"
14
14
  ---
15
15
 
16
16
  # gobi-media
17
17
 
18
- Gobi media generation commands (v2.0.30).
18
+ Gobi media generation commands (v2.0.32).
19
19
 
20
20
  Requires gobi-cli installed and authenticated. See gobi-core skill for setup.
21
21
 
@@ -7,12 +7,12 @@ description: >-
7
7
  allowed-tools: Bash(gobi:*)
8
8
  metadata:
9
9
  author: gobi-ai
10
- version: "2.0.30"
10
+ version: "2.0.32"
11
11
  ---
12
12
 
13
13
  # gobi-sense
14
14
 
15
- Gobi sense commands for activity and transcription data (v2.0.30).
15
+ Gobi sense commands for activity and transcription data (v2.0.32).
16
16
 
17
17
  Requires gobi-cli installed and authenticated. See the **gobi-core** skill for setup.
18
18
 
@@ -11,12 +11,12 @@ description: >-
11
11
  allowed-tools: Bash(gobi:*)
12
12
  metadata:
13
13
  author: gobi-ai
14
- version: "2.0.30"
14
+ version: "2.0.32"
15
15
  ---
16
16
 
17
17
  # gobi-space
18
18
 
19
- Gobi space, global, and personal-space posts (v2.0.30).
19
+ Gobi space, global, and personal-space posts (v2.0.32).
20
20
 
21
21
  Requires gobi-cli installed and authenticated. See the **gobi-core** skill for setup.
22
22
 
@@ -51,13 +51,15 @@ Posts have no vault attribution. Both `create-post` and `edit-post` across all t
51
51
 
52
52
  > **Wiki-link uploads moved to artifacts.** The `--auto-attachments` flag that used to upload `[[wiki-linked files]]` from a post body now lives on `gobi artifact create` / `gobi artifact revise` (markdown kinds). To publish a markdown creation with resolvable wikilinks, create a markdown artifact with `--vault-slug` + `--auto-attachments` and attach it to the post (`--post-id`). See the **gobi-artifact** skill. Before relying on wikilink resolution, confirm the anchor vault is published: `gobi --json vault status --vault-slug <slug>` should report `isPublished: true`.
53
53
 
54
- ## Post media attachments (`--attach`)
54
+ ## Post media + file attachments (`--attach`)
55
55
 
56
- `create-post` and `create-reply` across all three scopes (`gobi space`, `gobi global`, `gobi personal`) accept `--attach <file>` (repeatable) for inline post mediathe photos/GIF/video that render in-feed alongside the post body. The CLI uploads each file to S3 via `POST /posts/upload-url` and passes the resulting `{ mediaUrl, mediaKey }` array as the post's `attachments`.
56
+ `create-post` and `create-reply` across all three scopes (`gobi space`, `gobi global`, `gobi personal`) accept `--attach <file>` (repeatable) for inline post attachments — photos/GIF/video that render in-feed alongside the post body, and document files (**pdf/md/txt/csv**) that render as Slack-style file cards. The CLI uploads each file to S3 via `POST /posts/upload-url`; document files additionally carry `fileName` + `mimeType` on the row (the S3 key is a UUID, so that's the only place the original name survives).
57
57
 
58
- X-style mix rule (enforced client-side before upload): up to **4 photos** OR **1 GIF** OR **1 video** — they don't combine. Server-side ceilings: 5MB photos, 15MB GIFs, 512MB video.
58
+ Mix rule (enforced client-side before upload): up to **4 photos + 4 document files** together, OR **1 GIF**, OR **1 video** — GIF and video are exclusive with everything. Size ceilings: 10MB photos, 15MB GIFs, 512MB video, 250MB document files.
59
59
 
60
- Use `--attach` for media you want shown in the post itself; use a markdown **artifact** (`gobi artifact create`) for `[[wikilinks]]`-bearing creations attached to the post.
60
+ Use `--attach` for media/files you want shown in the post itself; use a markdown **artifact** (`gobi artifact create`) for `[[wikilinks]]`-bearing creations attached to the post.
61
+
62
+ **Reading attachments back:** feed and list lines show a compact marker like `📎 2 photos, 1 file`; `get-post` prints an `Attachments (N):` block with one line per attachment — kind, original fileName, dimensions/MIME, and the fetchable CDN URL (artifact attachments show kind/title/artifactId instead). In `--json` mode the full `attachments` array is on every post/reply object.
61
63
 
62
64
  ## Public link formats
63
65
 
@@ -106,7 +108,11 @@ gobi --json space list-posts
106
108
  - `gobi space list-topic-posts` — List posts tagged with a topic in a space (cursor-paginated).
107
109
 
108
110
  ### Feed
109
- - `gobi space feed` — List the unified feed (posts and replies, newest first).
111
+ - `gobi space feed` — List the unified feed (posts and replies, newest first). `--channel <channelId>` reads a channel's feed instead of the main feed.
112
+
113
+ ### Search
114
+ - `gobi space search-posts <query>` — Search a space's posts **and** replies, newest first. The query supports free-text keywords plus `from:<name>` (author) and `topic:<tag>` operators; quote multi-word values (`from:"Jane Doe"`). Each result is an individual post or reply, not a whole thread. `--channel <channelId>` restricts results to one channel; omit to search the main feed and every channel visible to you.
115
+ - `gobi personal search-posts <query>` — Same query syntax over your private personal-space posts and replies. There is no `gobi global` search.
110
116
 
111
117
  ### Space posts
112
118
  - `gobi space list-posts` — List posts in a space (paginated).
@@ -120,6 +126,22 @@ gobi --json space list-posts
120
126
  - `gobi space edit-reply <replyId>` — Edit a reply. You must be the author.
121
127
  - `gobi space delete-reply <replyId>` — Delete a reply. You must be the author.
122
128
 
129
+ ### Reactions
130
+
131
+ Emoji reactions on posts **and** replies, across all three scopes (`gobi space`, `gobi global`, `gobi personal`). The id is the bare number from the `[p:N]`/`[r:N]` ids in feed output. Feed and `get-post` lines render existing reactions as compact chips like `👍2* 🎉1` — the count follows each emoji, and a trailing `*` marks ones you reacted with.
132
+
133
+ - `gobi space react <postId> <emoji>` — Add a reaction (idempotent; re-reacting with the same emoji is a no-op).
134
+ - `gobi space unreact <postId> <emoji>` — Remove your reaction. Pass the emoji literally (`gobi space unreact 123 👍`).
135
+
136
+ ### Channels (space scope only)
137
+
138
+ Channels are private, member-gated sub-feeds inside a space. The **main feed is not a channel** — `feed` / `list-posts` / `create-post` without `--channel` target it, as always. Members see their own channels; space owners/admins see all of them; the space agent sees only channels with agent access enabled. Posting into a channel requires being able to see it. Channel administration (create, rename, delete, add/remove members, leave) is web-UI only — like space and member admin, the CLI deliberately doesn't expose it.
139
+
140
+ - `gobi space list-channels` — List channels visible to you (shows member count, your membership, agent access).
141
+ - `gobi space get-channel <channelId>` — Get one channel's details.
142
+ - `gobi space list-channel-members <channelId>` — List a channel's members.
143
+ - `--channel <channelId>` on `feed`, `list-posts`, and `create-post` reads/writes that channel instead of the main feed.
144
+
123
145
  ### Personal posts (global feed)
124
146
 
125
147
  `gobi global` is the same surface for Personal Posts — posts that live on the author's profile and surface in the public global feed.
@@ -141,6 +163,7 @@ gobi --json space list-posts
141
163
  A couple of read-side flags don't mirror — `personal feed` has no `--following` (there's no follow graph in a private space), and `personal list-posts` has no `--mine` (everything in the personal space is already yours).
142
164
 
143
165
  - `gobi personal feed` — Your personal-space feed (posts and replies, newest first).
166
+ - `gobi personal search-posts <query>` — Search your personal-space posts and replies (same `from:` / `topic:` query syntax as `gobi space search-posts`).
144
167
  - `gobi personal list-posts` — List your personal-space posts.
145
168
  - `gobi personal get-post <postId>` — Get a personal-space post with its ancestors and replies.
146
169
  - `gobi personal create-post` — Create a private personal-space post. Same flags as `gobi global create-post` (`--artifact`, `--repost-post-id`, `--attach`).
@@ -157,8 +180,9 @@ Most posts and replies are publicly visible — in a community space (`gobi spac
157
180
  - `create-post` / `create-reply` — content goes live on submission.
158
181
  - `edit-post` / `edit-reply` — confirm the *new* content; people who already saw the original may re-see it.
159
182
  - `delete-post` / `delete-reply` — irreversible. Flag that explicitly and confirm the target id before running.
183
+ - `react` / `unreact` are lightweight and reversible — when the user asked for the reaction, no extra confirmation needed.
160
184
 
161
- Read-only commands (`list-posts`, `get-post`, `feed`, `list-topics`, `list-topic-posts`, `get`) run without confirmation.
185
+ Read-only commands (`list-posts`, `get-post`, `feed`, `search-posts`, `list-topics`, `list-topic-posts`, `get`, `list-channels`, `get-channel`, `list-channel-members`) run without confirmation.
162
186
 
163
187
  ## Reference Documentation
164
188
 
@@ -18,6 +18,8 @@ Commands:
18
18
  create-reply [options] <postId> Create a reply to a post in the global feed.
19
19
  edit-reply [options] <replyId> Edit a reply you authored in the global feed.
20
20
  delete-reply <replyId> Delete a reply you authored in the global feed.
21
+ react <postId> <emoji> 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.
22
+ unreact <postId> <emoji> Remove your emoji reaction from a global-feed post or reply. <postId> is the numeric id of a post OR a reply.
21
23
  help [command] display help for command
22
24
  ```
23
25
 
@@ -75,7 +77,8 @@ Options:
75
77
  --content <content> Post content (markdown supported, use "-" for stdin)
76
78
  --rich-text <richText> Rich-text JSON array (mutually exclusive with --content)
77
79
  --artifact <artifactId> Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`. (default: [])
78
- --attach <file> Local media file to attach. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video. (default: [])
80
+ --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 /
81
+ 15MB GIFs / 512MB video / 250MB files. (default: [])
79
82
  --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
80
83
  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.
81
84
  -h, --help display help for command
@@ -92,8 +95,8 @@ Options:
92
95
  --title <title> New title
93
96
  --content <content> New content (markdown supported, use "-" for stdin)
94
97
  --rich-text <richText> Rich-text JSON array (mutually exclusive with --content)
95
- --attach <file> Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size
96
- ceilings: 5MB photos / 15MB GIFs / 512MB video. Omit to leave attachments unchanged. (default: [])
98
+ --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)
99
+ OR 1 GIF OR 1 video. Size ceilings: 10MB photos / 15MB GIFs / 512MB video / 250MB files. Omit to leave attachments unchanged. (default: [])
97
100
  --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
98
101
  with `gobi artifact create`. (default: [])
99
102
  -h, --help display help for command
@@ -120,8 +123,8 @@ Create a reply to a post in the global feed.
120
123
  Options:
121
124
  --content <content> Reply content (markdown supported, use "-" for stdin)
122
125
  --rich-text <richText> Rich-text JSON array (mutually exclusive with --content)
123
- --attach <file> Local media file to attach to this reply. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video. (default:
124
- [])
126
+ --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
127
+ photos / 15MB GIFs / 512MB video / 250MB files. (default: [])
125
128
  -h, --help display help for command
126
129
  ```
127
130
 
@@ -148,3 +151,25 @@ Delete a reply you authored in the global feed.
148
151
  Options:
149
152
  -h, --help display help for command
150
153
  ```
154
+
155
+ ## react
156
+
157
+ ```
158
+ Usage: gobi global react [options] <postId> <emoji>
159
+
160
+ 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.
161
+
162
+ Options:
163
+ -h, --help display help for command
164
+ ```
165
+
166
+ ## unreact
167
+
168
+ ```
169
+ Usage: gobi global unreact [options] <postId> <emoji>
170
+
171
+ Remove your emoji reaction from a global-feed post or reply. <postId> is the numeric id of a post OR a reply.
172
+
173
+ Options:
174
+ -h, --help display help for command
175
+ ```