@gobi-ai/cli 2.0.34 → 2.0.36

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.
@@ -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.34",
7
+ "version": "2.0.36",
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.34",
12
+ "version": "2.0.36",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gobi",
3
3
  "description": "Manage the Gobi collaborative knowledge platform from the command line",
4
- "version": "2.0.34",
4
+ "version": "2.0.36",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
package/README.md CHANGED
@@ -63,7 +63,7 @@ Each setup step unlocks a different family of commands — run only the ones the
63
63
  | Step | Unlocks |
64
64
  |------|---------|
65
65
  | `gobi auth login` | All authenticated commands |
66
- | `gobi vault init` | Every `gobi vault …` command (`publish`, `unpublish`, `sync`); also lets `artifact create --auto-attachments` resolve that vault automatically |
66
+ | `gobi vault init` | Every `gobi vault …` command (`publish`, `unpublish`, `sync`); also lets `<scope> artifact create --auto-attachments` resolve that vault automatically |
67
67
  | `gobi space warp` | Every `gobi space …` command without needing `--space-slug` |
68
68
 
69
69
  ---
@@ -233,19 +233,21 @@ Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
233
233
 
234
234
  An *artifact* is a versioned, human-owned creation attached to posts. Kinds: `image | video | gif | markdown | meeting_summary`. Markdown kinds carry a body; media kinds carry an uploaded file. Revisions form a draft/published tree (at most one published per artifact). Markdown kinds store `metadata.vaultSlug` for `[[wikilink]]` resolution. See the `gobi-artifact` skill for full workflows.
235
235
 
236
+ Artifacts are **scoped to a space**: the subcommands live under `gobi personal artifact …` (your personal space) and `gobi space artifact …` (the active team space — `gobi space warp <slug>` or `gobi space --space-slug <slug> artifact …`). `<scope>` below is `personal` or `space`.
237
+
236
238
  | Command | Description |
237
239
  |---------|-------------|
238
- | `gobi artifact list [--kind <k>] [--limit N]` | List your artifacts (newest first) |
239
- | `gobi artifact get <artifactId>` | Get one artifact with its current revision |
240
- | `gobi artifact create --kind <k> [--file <path> \| --content <md>] [--title <t>] [--vault-slug <slug>] [--post-id <id>] [--auto-attachments] [--change-note <note>]` | Create an artifact. markdown/meeting_summary take a body via `--file`, `--content`, or stdin (`-`); image/gif/video upload `--file`. `--post-id` attaches it to a post (appends, doesn't clobber). `--auto-attachments` (markdown) uploads `[[wikilinks]]` to `--vault-slug`. |
241
- | `gobi artifact revise <artifactId> [--file <path> \| --content <md>] [--change-note <note>] [--from <revisionId>] [--auto-attachments]` | Add a draft revision. `--from` branches off a specific revision. `--auto-attachments` reuses the artifact's stored `metadata.vaultSlug`. |
242
- | `gobi artifact publish <artifactId> --revision <revisionId>` | Publish a revision (the artifact's single published revision) |
243
- | `gobi artifact revert <artifactId> --to <revisionId>` | Move the published pointer to an earlier revision |
244
- | `gobi artifact history <artifactId>` | List the full revision tree (owner only) |
245
- | `gobi artifact download <artifactId> [--revision <revisionId>] [--out <path>]` | Download a revision's content (markdown body to file/stdout; media bytes to file). Defaults to the current revision. |
246
- | `gobi artifact delete <artifactId>` | Delete an artifact and its revision tree |
247
-
248
- Attach an artifact to a post at creation time with `gobi artifact create --post-id <postId>` (it merges into the post's existing artifacts without clobbering them).
240
+ | `gobi <scope> artifact list [--kind <k>] [--limit N]` | List this scope's artifacts (newest first) |
241
+ | `gobi <scope> artifact get <artifactId>` | Get one artifact with its current revision |
242
+ | `gobi <scope> artifact create --kind <k> [--file <path> \| --content <md>] [--title <t>] [--vault-slug <slug>] [--post-id <id>] [--auto-attachments] [--change-note <note>]` | Create an artifact in this scope. markdown/meeting_summary take a body via `--file`, `--content`, or stdin (`-`); image/gif/video upload `--file`. `--post-id` attaches it to a post (appends, doesn't clobber). `--auto-attachments` (markdown) uploads `[[wikilinks]]` to `--vault-slug`. |
243
+ | `gobi <scope> artifact revise <artifactId> [--file <path> \| --content <md>] [--change-note <note>] [--from <revisionId>] [--auto-attachments]` | Add a draft revision. `--from` branches off a specific revision. `--auto-attachments` reuses the artifact's stored `metadata.vaultSlug`. |
244
+ | `gobi <scope> artifact publish <artifactId> --revision <revisionId>` | Publish a revision (the artifact's single published revision) |
245
+ | `gobi <scope> artifact revert <artifactId> --to <revisionId>` | Move the published pointer to an earlier revision |
246
+ | `gobi <scope> artifact history <artifactId>` | List the full revision tree (owner only) |
247
+ | `gobi <scope> artifact download <artifactId> [--revision <revisionId>] [--out <path>]` | Download a revision's content (markdown body to file/stdout; media bytes to file). Defaults to the current revision. |
248
+ | `gobi <scope> artifact delete <artifactId>` | Delete an artifact and its revision tree |
249
+
250
+ Attach an artifact to a post at creation time with `gobi <scope> artifact create --post-id <postId>` (it merges into the post's existing artifacts without clobbering them).
249
251
 
250
252
  ### Media generation
251
253
 
@@ -303,7 +305,7 @@ The CLI ships a `.claude-plugin/` manifest with skills that wrap the command gro
303
305
  | `gobi-core` | Auth, update, space list/warp |
304
306
  | `gobi-vault` | `gobi vault init/list/publish/unpublish/sync` |
305
307
  | `gobi-space` | `gobi space …`, `gobi global …`, and `gobi personal …` |
306
- | `gobi-artifact` | `gobi artifact …` |
308
+ | `gobi-artifact` | `gobi personal artifact …` and `gobi space artifact …` |
307
309
  | `gobi-media` | `gobi media …` |
308
310
  | `gobi-sense` | `gobi sense list-activities/list-transcriptions` |
309
311
  | `gobi-homepage` | Building custom HTML homepages with `window.gobi` |
@@ -135,11 +135,11 @@ function printArtifact(a) {
135
135
  console.log(` mediaUrl: ${rev.mediaUrl}`);
136
136
  }
137
137
  }
138
- export function registerArtifactCommand(program) {
139
- const artifact = program
140
- .command("artifact")
141
- .description("Versioned creations attached to posts. Kinds: image | video | gif | markdown | meeting_summary. " +
142
- "Always human-owned; revisions form a draft/published tree (at most one published per artifact).");
138
+ // Registers the full `artifact` subcommand tree under `parent` (a `gobi space`
139
+ // or `gobi personal` group). Moved here from a top-level `gobi artifact` group
140
+ // so artifacts are scoped to a space (team or personal), matching posts.
141
+ export function registerArtifactSubcommands(parent, scope, description) {
142
+ const artifact = parent.command("artifact").description(description);
143
143
  // ── Create ──
144
144
  artifact
145
145
  .command("create")
@@ -158,6 +158,10 @@ export function registerArtifactCommand(program) {
158
158
  throw new Error(`--kind must be one of: ${ALL_KINDS.join(", ")}`);
159
159
  }
160
160
  const body = { kind };
161
+ // Scope the new artifact to this group's space (team) or personal space.
162
+ const { spaceSlug } = scope.resolve();
163
+ if (spaceSlug)
164
+ body.spaceSlug = spaceSlug;
161
165
  if (opts.title != null)
162
166
  body.title = opts.title;
163
167
  if (opts.vaultSlug)
@@ -421,11 +425,15 @@ export function registerArtifactCommand(program) {
421
425
  // ── List ──
422
426
  artifact
423
427
  .command("list")
424
- .description("List your artifacts (newest first).")
428
+ .description("List this scope's artifacts (newest first).")
425
429
  .option("--kind <kind>", `Filter by kind: ${ALL_KINDS.join(" | ")}`)
426
430
  .option("--limit <n>", "Max items to return")
427
431
  .action(async (opts) => {
428
432
  const params = {};
433
+ // Scope the listing to this group's space (team) or personal space.
434
+ const { spaceSlug } = scope.resolve();
435
+ if (spaceSlug)
436
+ params.spaceSlug = spaceSlug;
429
437
  if (opts.kind) {
430
438
  if (!ALL_KINDS.includes(opts.kind)) {
431
439
  throw new Error(`--kind must be one of: ${ALL_KINDS.join(", ")}`);
@@ -1,6 +1,6 @@
1
1
  import { WEB_BASE_URL } from "../constants.js";
2
2
  import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
3
- import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
3
+ import { buildMentionMap, formatAttachmentLines, formatAttachmentSummary, formatPostLabel, formatReactionChips, formatReplyLine, isJsonMode, jsonOut, postBodyText, readStdin, unwrapResp, } from "./utils.js";
4
4
  import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
5
5
  function readContent(value) {
6
6
  if (value === "-")
@@ -10,7 +10,7 @@ function readContent(value) {
10
10
  function buildPersonalPostUrl(post) {
11
11
  return `${WEB_BASE_URL}/posts/${post.id}`;
12
12
  }
13
- function formatFeedLine(m) {
13
+ function formatFeedLine(m, mentions) {
14
14
  const isReply = m.parentPostId != null ||
15
15
  m.type === "post-reply";
16
16
  const id = `[${isReply ? "r" : "p"}:${m.id}]`;
@@ -19,12 +19,11 @@ function formatFeedLine(m) {
19
19
  `User ${m.authorId ?? "?"}`;
20
20
  let label;
21
21
  if (isReply) {
22
- const text = m.content || "";
22
+ const text = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
23
23
  label = text.length > 80 ? text.slice(0, 80) + "…" : text;
24
- label = label.replace(/\s+/g, " ").trim();
25
24
  }
26
25
  else {
27
- label = m.title || m.content || "";
26
+ label = formatPostLabel(m, mentions);
28
27
  }
29
28
  const chips = formatReactionChips(m);
30
29
  const attachSummary = formatAttachmentSummary(m);
@@ -56,6 +55,7 @@ export function registerGlobalCommand(program) {
56
55
  jsonOut({
57
56
  items: resp.data || [],
58
57
  pagination: resp.pagination || {},
58
+ mentions: resp.mentions || {},
59
59
  });
60
60
  return;
61
61
  }
@@ -65,7 +65,8 @@ export function registerGlobalCommand(program) {
65
65
  console.log("No items found.");
66
66
  return;
67
67
  }
68
- const lines = items.map(formatFeedLine);
68
+ const mentions = buildMentionMap(resp);
69
+ const lines = items.map((m) => formatFeedLine(m, mentions));
69
70
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
70
71
  console.log(`Global feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
71
72
  });
@@ -89,6 +90,7 @@ export function registerGlobalCommand(program) {
89
90
  jsonOut({
90
91
  items: resp.data || [],
91
92
  pagination: resp.pagination || {},
93
+ mentions: resp.mentions || {},
92
94
  });
93
95
  return;
94
96
  }
@@ -98,11 +100,19 @@ export function registerGlobalCommand(program) {
98
100
  console.log("No posts found.");
99
101
  return;
100
102
  }
103
+ const mentions = buildMentionMap(resp);
101
104
  const lines = [];
102
105
  for (const t of items) {
103
106
  const author = t.author?.name ||
104
107
  `User ${t.authorId}`;
105
- lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount ?? 0} replies, ${t.createdAt})`);
108
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" by ${author} (${t.replyCount ?? 0} replies, ${t.createdAt})`);
109
+ for (const line of formatAttachmentLines(t, " ", "📎")) {
110
+ lines.push(line);
111
+ }
112
+ const replies = t.replies || [];
113
+ for (const r of replies) {
114
+ lines.push(formatReplyLine(r, mentions));
115
+ }
106
116
  }
107
117
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
108
118
  console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
@@ -135,19 +145,20 @@ export function registerGlobalCommand(program) {
135
145
  }
136
146
  const post = (data.update || data.post || data);
137
147
  const replies = (data.replies || []);
148
+ const mentionMap = buildMentionMap(postResp);
138
149
  const author = post.author?.name ||
139
150
  `User ${post.authorId}`;
140
151
  const ancestorLines = [];
141
152
  if (ancestors.length) {
142
153
  ancestors.forEach((a, i) => {
143
- ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
154
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a, mentionMap)}`);
144
155
  });
145
156
  }
146
157
  const replyLines = [];
147
158
  for (const r of replies) {
148
159
  const rAuthor = r.author?.name ||
149
160
  `User ${r.authorId}`;
150
- const text = r.content || "";
161
+ const text = postBodyText(r, mentionMap);
151
162
  const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
152
163
  const rChips = formatReactionChips(r);
153
164
  const rAttach = formatAttachmentSummary(r);
@@ -167,7 +178,7 @@ export function registerGlobalCommand(program) {
167
178
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
168
179
  : []),
169
180
  "",
170
- post.content || "",
181
+ postBodyText(post, mentionMap),
171
182
  ...(attachmentLines.length
172
183
  ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
173
184
  : []),
@@ -187,7 +198,7 @@ export function registerGlobalCommand(program) {
187
198
  .option("--title <title>", "Title of the post")
188
199
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
189
200
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
190
- .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
201
+ .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi personal artifact create`.", (value, prev = []) => [...prev, value], [])
191
202
  .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], [])
192
203
  .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.")
193
204
  .action(async (opts) => {
@@ -253,7 +264,7 @@ export function registerGlobalCommand(program) {
253
264
  .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
254
265
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
255
266
  .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], [])
256
- .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], [])
267
+ .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 personal artifact create`.", (value, prev = []) => [...prev, value], [])
257
268
  .action(async (postId, opts) => {
258
269
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
259
270
  const wantsArtifactChange = !!(opts.artifact && opts.artifact.length > 0);
@@ -1,12 +1,13 @@
1
1
  import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
2
- import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, unwrapResp, } from "./utils.js";
2
+ import { buildMentionMap, formatAttachmentLines, formatAttachmentSummary, formatPostLabel, formatReactionChips, formatReplyLine, isJsonMode, jsonOut, postBodyText, readStdin, unwrapResp, } from "./utils.js";
3
3
  import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
4
+ import { registerArtifactSubcommands } from "./artifact.js";
4
5
  function readContent(value) {
5
6
  if (value === "-")
6
7
  return readStdin();
7
8
  return value;
8
9
  }
9
- function formatFeedLine(m) {
10
+ function formatFeedLine(m, mentions) {
10
11
  const isReply = m.parentPostId != null ||
11
12
  m.type === "post-reply";
12
13
  const id = `[${isReply ? "r" : "p"}:${m.id}]`;
@@ -15,12 +16,11 @@ function formatFeedLine(m) {
15
16
  `User ${m.authorId ?? "?"}`;
16
17
  let label;
17
18
  if (isReply) {
18
- const text = m.content || "";
19
+ const text = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
19
20
  label = text.length > 80 ? text.slice(0, 80) + "…" : text;
20
- label = label.replace(/\s+/g, " ").trim();
21
21
  }
22
22
  else {
23
- label = m.title || m.content || "";
23
+ label = formatPostLabel(m, mentions);
24
24
  }
25
25
  const chips = formatReactionChips(m);
26
26
  const attachSummary = formatAttachmentSummary(m);
@@ -51,6 +51,7 @@ export function registerPersonalCommand(program) {
51
51
  jsonOut({
52
52
  items: resp.data || [],
53
53
  pagination: resp.pagination || {},
54
+ mentions: resp.mentions || {},
54
55
  });
55
56
  return;
56
57
  }
@@ -60,7 +61,8 @@ export function registerPersonalCommand(program) {
60
61
  console.log("No items in your personal space yet.");
61
62
  return;
62
63
  }
63
- const lines = items.map(formatFeedLine);
64
+ const mentions = buildMentionMap(resp);
65
+ const lines = items.map((m) => formatFeedLine(m, mentions));
64
66
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
65
67
  console.log(`Personal-space feed (${items.length} items, newest first):\n` +
66
68
  lines.join("\n") +
@@ -96,7 +98,8 @@ export function registerPersonalCommand(program) {
96
98
  console.log("No results found.");
97
99
  return;
98
100
  }
99
- const lines = items.map(formatFeedLine);
101
+ const mentions = buildMentionMap(resp);
102
+ const lines = items.map((m) => formatFeedLine(m, mentions));
100
103
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
101
104
  console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
102
105
  });
@@ -129,9 +132,31 @@ export function registerPersonalCommand(program) {
129
132
  console.log("No posts found in your personal space.");
130
133
  return;
131
134
  }
135
+ const mentions = buildMentionMap(resp);
136
+ // The personal feed returns posts and replies as a flat list; group the
137
+ // replies under their root post so they can be nested. Falls back to an
138
+ // embedded `replies` array if the endpoint provides one.
139
+ const repliesByRoot = new Map();
140
+ for (const it of allItems) {
141
+ if (it.type === "post-reply" || it.parentPostId != null) {
142
+ const root = it.rootPostId ?? it.parentPostId;
143
+ const arr = repliesByRoot.get(root) || [];
144
+ arr.push(it);
145
+ repliesByRoot.set(root, arr);
146
+ }
147
+ }
132
148
  const lines = [];
133
149
  for (const t of items) {
134
- lines.push(`- [${t.id}] "${t.title ?? "(no title)"}" (${t.replyCount ?? 0} replies, ${t.createdAt})`);
150
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" (${t.replyCount ?? 0} replies, ${t.createdAt})`);
151
+ for (const line of formatAttachmentLines(t, " ", "📎")) {
152
+ lines.push(line);
153
+ }
154
+ const replies = t.replies ||
155
+ repliesByRoot.get(t.id) ||
156
+ [];
157
+ for (const r of replies) {
158
+ lines.push(formatReplyLine(r, mentions));
159
+ }
135
160
  }
136
161
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
137
162
  console.log(`Personal-space posts (${items.length} of ${allItems.length} feed items):\n` +
@@ -171,19 +196,20 @@ export function registerPersonalCommand(program) {
171
196
  }
172
197
  const post = (data.update || data.post || data);
173
198
  const replies = (data.replies || []);
199
+ const mentionMap = buildMentionMap(postResp);
174
200
  const author = post.author?.name ||
175
201
  `User ${post.authorId}`;
176
202
  const ancestorLines = [];
177
203
  if (ancestors.length) {
178
204
  ancestors.forEach((a, i) => {
179
- ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
205
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a, mentionMap)}`);
180
206
  });
181
207
  }
182
208
  const replyLines = [];
183
209
  for (const r of replies) {
184
210
  const rAuthor = r.author?.name ||
185
211
  `User ${r.authorId}`;
186
- const text = r.content || "";
212
+ const text = postBodyText(r, mentionMap);
187
213
  const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
188
214
  const rChips = formatReactionChips(r);
189
215
  const rAttach = formatAttachmentSummary(r);
@@ -203,7 +229,7 @@ export function registerPersonalCommand(program) {
203
229
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
204
230
  : []),
205
231
  "",
206
- post.content || "",
232
+ postBodyText(post, mentionMap),
207
233
  ...(attachmentLines.length
208
234
  ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
209
235
  : []),
@@ -229,7 +255,7 @@ export function registerPersonalCommand(program) {
229
255
  .option("--title <title>", "Title of the post")
230
256
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
231
257
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
232
- .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
258
+ .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi personal artifact create`.", (value, prev = []) => [...prev, value], [])
233
259
  .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], [])
234
260
  .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.")
235
261
  .action(async (opts) => {
@@ -299,7 +325,7 @@ export function registerPersonalCommand(program) {
299
325
  .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
300
326
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
301
327
  .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], [])
302
- .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], [])
328
+ .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 personal artifact create`.", (value, prev = []) => [...prev, value], [])
303
329
  .action(async (postId, opts) => {
304
330
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
305
331
  const wantsArtifactChange = !!(opts.artifact && opts.artifact.length > 0);
@@ -475,4 +501,8 @@ export function registerPersonalCommand(program) {
475
501
  console.log(`Removed ${emoji} reaction from ${postId}.` +
476
502
  (chips ? `\n Now: ${chips}` : ""));
477
503
  });
504
+ // ── Artifacts (scoped to your personal space) ──
505
+ registerArtifactSubcommands(personal, { resolve: () => ({}) }, "Versioned creations attached to posts, scoped to your personal space (visible " +
506
+ "only to you). Kinds: image | video | gif | markdown | meeting_summary. Always " +
507
+ "human-owned; revisions form a draft/published tree (one published per artifact).");
478
508
  }
@@ -1,14 +1,15 @@
1
1
  import { WEB_BASE_URL } from "../constants.js";
2
2
  import { apiGet, apiPost, apiPatch, apiPut, apiDelete } from "../client.js";
3
3
  import { requireSpace, selectSpace, setSpaceRequirement, writeSpaceSetting, } from "./init.js";
4
- import { formatAttachmentLines, formatAttachmentSummary, formatReactionChips, isJsonMode, jsonOut, readStdin, resolveSpaceSlug, unwrapResp, } from "./utils.js";
4
+ import { buildMentionMap, formatAttachmentLines, formatAttachmentSummary, formatPostLabel, formatReactionChips, formatReplyLine, isJsonMode, jsonOut, postBodyText, readStdin, resolveSpaceSlug, unwrapResp, } from "./utils.js";
5
5
  import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
6
+ import { registerArtifactSubcommands } from "./artifact.js";
6
7
  function readContent(value) {
7
8
  if (value === "-")
8
9
  return readStdin();
9
10
  return value;
10
11
  }
11
- function formatFeedLine(m) {
12
+ function formatFeedLine(m, mentions) {
12
13
  const isReply = m.parentPostId != null;
13
14
  const id = `[${isReply ? "r" : "p"}:${m.id}]`;
14
15
  const kind = isReply ? "reply" : "post ";
@@ -16,12 +17,11 @@ function formatFeedLine(m) {
16
17
  `User ${m.authorId ?? "?"}`;
17
18
  let label;
18
19
  if (isReply) {
19
- const text = m.content || "";
20
+ const text = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
20
21
  label = text.length > 80 ? text.slice(0, 80) + "…" : text;
21
- label = label.replace(/\s+/g, " ").trim();
22
22
  }
23
23
  else {
24
- label = m.title || m.content || "";
24
+ label = formatPostLabel(m, mentions);
25
25
  }
26
26
  const chips = formatReactionChips(m);
27
27
  const attachSummary = formatAttachmentSummary(m);
@@ -174,11 +174,12 @@ export function registerSpaceCommand(program) {
174
174
  console.log(`No posts found for topic "${topic.name || topicSlug}".`);
175
175
  return;
176
176
  }
177
+ const mentions = buildMentionMap(resp);
177
178
  const lines = [];
178
179
  for (const t of posts) {
179
180
  const author = t.author?.name || "Unknown";
180
181
  const spaceName = t.space?.name || "";
181
- lines.push(`- [${t.id}] "${t.title}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
182
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
182
183
  }
183
184
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
184
185
  console.log(`Topic: ${topic.name || topicSlug}\n` +
@@ -216,7 +217,8 @@ export function registerSpaceCommand(program) {
216
217
  console.log("No items found.");
217
218
  return;
218
219
  }
219
- const lines = items.map(formatFeedLine);
220
+ const mentions = buildMentionMap(resp);
221
+ const lines = items.map((m) => formatFeedLine(m, mentions));
220
222
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
221
223
  console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
222
224
  });
@@ -254,7 +256,8 @@ export function registerSpaceCommand(program) {
254
256
  console.log("No results found.");
255
257
  return;
256
258
  }
257
- const lines = items.map(formatFeedLine);
259
+ const mentions = buildMentionMap(resp);
260
+ const lines = items.map((m) => formatFeedLine(m, mentions));
258
261
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
259
262
  console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
260
263
  });
@@ -286,21 +289,22 @@ export function registerSpaceCommand(program) {
286
289
  jsonOut({ ...data, ancestors, pagination, mentions });
287
290
  return;
288
291
  }
289
- const post = (data.thread || data);
292
+ const post = (data.post || data.thread || data);
290
293
  const replies = (data.items || []);
294
+ const mentionMap = buildMentionMap(postResp);
291
295
  const author = post.author?.name ||
292
296
  `User ${post.authorId}`;
293
297
  const ancestorLines = [];
294
298
  if (ancestors.length) {
295
299
  ancestors.forEach((a, i) => {
296
- ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
300
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a, mentionMap)}`);
297
301
  });
298
302
  }
299
303
  const replyLines = [];
300
304
  for (const r of replies) {
301
305
  const rAuthor = r.author?.name ||
302
306
  `User ${r.authorId}`;
303
- const text = r.content;
307
+ const text = postBodyText(r, mentionMap);
304
308
  const body = opts.full || !text || text.length <= 200
305
309
  ? text
306
310
  : text.slice(0, 200) + "…";
@@ -322,7 +326,7 @@ export function registerSpaceCommand(program) {
322
326
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
323
327
  : []),
324
328
  "",
325
- post.content || "",
329
+ postBodyText(post, mentionMap),
326
330
  ...(attachmentLines.length
327
331
  ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
328
332
  : []),
@@ -363,12 +367,19 @@ export function registerSpaceCommand(program) {
363
367
  console.log("No posts found.");
364
368
  return;
365
369
  }
370
+ const mentions = buildMentionMap(resp);
366
371
  const lines = [];
367
372
  for (const t of items) {
368
373
  const author = t.author?.name ||
369
374
  `User ${t.authorId}`;
370
- const attachSummary = formatAttachmentSummary(t);
371
- lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})${attachSummary ? ` ${attachSummary}` : ""}`);
375
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
376
+ for (const line of formatAttachmentLines(t, " ", "📎")) {
377
+ lines.push(line);
378
+ }
379
+ const replies = t.replies || [];
380
+ for (const r of replies) {
381
+ lines.push(formatReplyLine(r, mentions));
382
+ }
372
383
  }
373
384
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
374
385
  console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
@@ -379,7 +390,7 @@ export function registerSpaceCommand(program) {
379
390
  .option("--title <title>", "Title of the post")
380
391
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
381
392
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
382
- .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi artifact create`.", (value, prev = []) => [...prev, value], [])
393
+ .option("--artifact <artifactId>", "Attach an existing artifact to the post (repeatable). Create artifacts with `gobi space artifact create`.", (value, prev = []) => [...prev, value], [])
383
394
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
384
395
  .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], [])
385
396
  .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.")
@@ -452,7 +463,7 @@ export function registerSpaceCommand(program) {
452
463
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
453
464
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
454
465
  .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], [])
455
- .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], [])
466
+ .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 space artifact create`.", (value, prev = []) => [...prev, value], [])
456
467
  .action(async (postId, opts) => {
457
468
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
458
469
  const wantsArtifactChange = !!(opts.artifact && opts.artifact.length > 0);
@@ -714,4 +725,8 @@ export function registerSpaceCommand(program) {
714
725
  }
715
726
  console.log(`Channel members (${items.length}):\n` + lines.join("\n"));
716
727
  });
728
+ // ── Artifacts (scoped to this space) ──
729
+ registerArtifactSubcommands(space, { resolve: () => ({ spaceSlug: resolveSpaceSlug(space) }) }, "Versioned creations attached to posts, scoped to this space (visible to its " +
730
+ "members). Kinds: image | video | gif | markdown | meeting_summary. Always " +
731
+ "human-owned; revisions form a draft/published tree (one published per artifact).");
717
732
  }