@gobi-ai/cli 2.0.34 → 2.0.35

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.35",
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.35",
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.35",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
@@ -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
  : []),
@@ -1,12 +1,12 @@
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
4
  function readContent(value) {
5
5
  if (value === "-")
6
6
  return readStdin();
7
7
  return value;
8
8
  }
9
- function formatFeedLine(m) {
9
+ function formatFeedLine(m, mentions) {
10
10
  const isReply = m.parentPostId != null ||
11
11
  m.type === "post-reply";
12
12
  const id = `[${isReply ? "r" : "p"}:${m.id}]`;
@@ -15,12 +15,11 @@ function formatFeedLine(m) {
15
15
  `User ${m.authorId ?? "?"}`;
16
16
  let label;
17
17
  if (isReply) {
18
- const text = m.content || "";
18
+ const text = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
19
19
  label = text.length > 80 ? text.slice(0, 80) + "…" : text;
20
- label = label.replace(/\s+/g, " ").trim();
21
20
  }
22
21
  else {
23
- label = m.title || m.content || "";
22
+ label = formatPostLabel(m, mentions);
24
23
  }
25
24
  const chips = formatReactionChips(m);
26
25
  const attachSummary = formatAttachmentSummary(m);
@@ -51,6 +50,7 @@ export function registerPersonalCommand(program) {
51
50
  jsonOut({
52
51
  items: resp.data || [],
53
52
  pagination: resp.pagination || {},
53
+ mentions: resp.mentions || {},
54
54
  });
55
55
  return;
56
56
  }
@@ -60,7 +60,8 @@ export function registerPersonalCommand(program) {
60
60
  console.log("No items in your personal space yet.");
61
61
  return;
62
62
  }
63
- const lines = items.map(formatFeedLine);
63
+ const mentions = buildMentionMap(resp);
64
+ const lines = items.map((m) => formatFeedLine(m, mentions));
64
65
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
65
66
  console.log(`Personal-space feed (${items.length} items, newest first):\n` +
66
67
  lines.join("\n") +
@@ -96,7 +97,8 @@ export function registerPersonalCommand(program) {
96
97
  console.log("No results found.");
97
98
  return;
98
99
  }
99
- const lines = items.map(formatFeedLine);
100
+ const mentions = buildMentionMap(resp);
101
+ const lines = items.map((m) => formatFeedLine(m, mentions));
100
102
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
101
103
  console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
102
104
  });
@@ -129,9 +131,31 @@ export function registerPersonalCommand(program) {
129
131
  console.log("No posts found in your personal space.");
130
132
  return;
131
133
  }
134
+ const mentions = buildMentionMap(resp);
135
+ // The personal feed returns posts and replies as a flat list; group the
136
+ // replies under their root post so they can be nested. Falls back to an
137
+ // embedded `replies` array if the endpoint provides one.
138
+ const repliesByRoot = new Map();
139
+ for (const it of allItems) {
140
+ if (it.type === "post-reply" || it.parentPostId != null) {
141
+ const root = it.rootPostId ?? it.parentPostId;
142
+ const arr = repliesByRoot.get(root) || [];
143
+ arr.push(it);
144
+ repliesByRoot.set(root, arr);
145
+ }
146
+ }
132
147
  const lines = [];
133
148
  for (const t of items) {
134
- lines.push(`- [${t.id}] "${t.title ?? "(no title)"}" (${t.replyCount ?? 0} replies, ${t.createdAt})`);
149
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" (${t.replyCount ?? 0} replies, ${t.createdAt})`);
150
+ for (const line of formatAttachmentLines(t, " ", "📎")) {
151
+ lines.push(line);
152
+ }
153
+ const replies = t.replies ||
154
+ repliesByRoot.get(t.id) ||
155
+ [];
156
+ for (const r of replies) {
157
+ lines.push(formatReplyLine(r, mentions));
158
+ }
135
159
  }
136
160
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
137
161
  console.log(`Personal-space posts (${items.length} of ${allItems.length} feed items):\n` +
@@ -171,19 +195,20 @@ export function registerPersonalCommand(program) {
171
195
  }
172
196
  const post = (data.update || data.post || data);
173
197
  const replies = (data.replies || []);
198
+ const mentionMap = buildMentionMap(postResp);
174
199
  const author = post.author?.name ||
175
200
  `User ${post.authorId}`;
176
201
  const ancestorLines = [];
177
202
  if (ancestors.length) {
178
203
  ancestors.forEach((a, i) => {
179
- ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
204
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a, mentionMap)}`);
180
205
  });
181
206
  }
182
207
  const replyLines = [];
183
208
  for (const r of replies) {
184
209
  const rAuthor = r.author?.name ||
185
210
  `User ${r.authorId}`;
186
- const text = r.content || "";
211
+ const text = postBodyText(r, mentionMap);
187
212
  const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
188
213
  const rChips = formatReactionChips(r);
189
214
  const rAttach = formatAttachmentSummary(r);
@@ -203,7 +228,7 @@ export function registerPersonalCommand(program) {
203
228
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
204
229
  : []),
205
230
  "",
206
- post.content || "",
231
+ postBodyText(post, mentionMap),
207
232
  ...(attachmentLines.length
208
233
  ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
209
234
  : []),
@@ -1,14 +1,14 @@
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
6
  function readContent(value) {
7
7
  if (value === "-")
8
8
  return readStdin();
9
9
  return value;
10
10
  }
11
- function formatFeedLine(m) {
11
+ function formatFeedLine(m, mentions) {
12
12
  const isReply = m.parentPostId != null;
13
13
  const id = `[${isReply ? "r" : "p"}:${m.id}]`;
14
14
  const kind = isReply ? "reply" : "post ";
@@ -16,12 +16,11 @@ function formatFeedLine(m) {
16
16
  `User ${m.authorId ?? "?"}`;
17
17
  let label;
18
18
  if (isReply) {
19
- const text = m.content || "";
19
+ const text = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
20
20
  label = text.length > 80 ? text.slice(0, 80) + "…" : text;
21
- label = label.replace(/\s+/g, " ").trim();
22
21
  }
23
22
  else {
24
- label = m.title || m.content || "";
23
+ label = formatPostLabel(m, mentions);
25
24
  }
26
25
  const chips = formatReactionChips(m);
27
26
  const attachSummary = formatAttachmentSummary(m);
@@ -174,11 +173,12 @@ export function registerSpaceCommand(program) {
174
173
  console.log(`No posts found for topic "${topic.name || topicSlug}".`);
175
174
  return;
176
175
  }
176
+ const mentions = buildMentionMap(resp);
177
177
  const lines = [];
178
178
  for (const t of posts) {
179
179
  const author = t.author?.name || "Unknown";
180
180
  const spaceName = t.space?.name || "";
181
- lines.push(`- [${t.id}] "${t.title}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
181
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
182
182
  }
183
183
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
184
184
  console.log(`Topic: ${topic.name || topicSlug}\n` +
@@ -216,7 +216,8 @@ export function registerSpaceCommand(program) {
216
216
  console.log("No items found.");
217
217
  return;
218
218
  }
219
- const lines = items.map(formatFeedLine);
219
+ const mentions = buildMentionMap(resp);
220
+ const lines = items.map((m) => formatFeedLine(m, mentions));
220
221
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
221
222
  console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
222
223
  });
@@ -254,7 +255,8 @@ export function registerSpaceCommand(program) {
254
255
  console.log("No results found.");
255
256
  return;
256
257
  }
257
- const lines = items.map(formatFeedLine);
258
+ const mentions = buildMentionMap(resp);
259
+ const lines = items.map((m) => formatFeedLine(m, mentions));
258
260
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
259
261
  console.log(`Search results (${items.length} items, newest first):\n` + lines.join("\n") + footer);
260
262
  });
@@ -286,21 +288,22 @@ export function registerSpaceCommand(program) {
286
288
  jsonOut({ ...data, ancestors, pagination, mentions });
287
289
  return;
288
290
  }
289
- const post = (data.thread || data);
291
+ const post = (data.post || data.thread || data);
290
292
  const replies = (data.items || []);
293
+ const mentionMap = buildMentionMap(postResp);
291
294
  const author = post.author?.name ||
292
295
  `User ${post.authorId}`;
293
296
  const ancestorLines = [];
294
297
  if (ancestors.length) {
295
298
  ancestors.forEach((a, i) => {
296
- ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
299
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a, mentionMap)}`);
297
300
  });
298
301
  }
299
302
  const replyLines = [];
300
303
  for (const r of replies) {
301
304
  const rAuthor = r.author?.name ||
302
305
  `User ${r.authorId}`;
303
- const text = r.content;
306
+ const text = postBodyText(r, mentionMap);
304
307
  const body = opts.full || !text || text.length <= 200
305
308
  ? text
306
309
  : text.slice(0, 200) + "…";
@@ -322,7 +325,7 @@ export function registerSpaceCommand(program) {
322
325
  ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
323
326
  : []),
324
327
  "",
325
- post.content || "",
328
+ postBodyText(post, mentionMap),
326
329
  ...(attachmentLines.length
327
330
  ? ["", `Attachments (${attachmentLines.length}):`, ...attachmentLines]
328
331
  : []),
@@ -363,12 +366,19 @@ export function registerSpaceCommand(program) {
363
366
  console.log("No posts found.");
364
367
  return;
365
368
  }
369
+ const mentions = buildMentionMap(resp);
366
370
  const lines = [];
367
371
  for (const t of items) {
368
372
  const author = t.author?.name ||
369
373
  `User ${t.authorId}`;
370
- const attachSummary = formatAttachmentSummary(t);
371
- lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})${attachSummary ? ` ${attachSummary}` : ""}`);
374
+ lines.push(`- [${t.id}] "${formatPostLabel(t, mentions)}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
375
+ for (const line of formatAttachmentLines(t, " ", "📎")) {
376
+ lines.push(line);
377
+ }
378
+ const replies = t.replies || [];
379
+ for (const r of replies) {
380
+ lines.push(formatReplyLine(r, mentions));
381
+ }
372
382
  }
373
383
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
374
384
  console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
@@ -38,6 +38,89 @@ export function formatReactionChips(m) {
38
38
  .map((r) => `${r.emoji}${r.count}${r.reactedByMe ? "*" : ""}`)
39
39
  .join(" ");
40
40
  }
41
+ // Build a userId -> name lookup from a list/feed/thread response's `mentions`
42
+ // block. Returns an empty map when absent, so callers can pass it through
43
+ // unconditionally and mentions just fall back to `@<id>`.
44
+ export function buildMentionMap(resp) {
45
+ const map = new Map();
46
+ // `mentions` sits at the top level on raw list/feed responses but under
47
+ // `data` on unwrapped thread/topic payloads — accept either.
48
+ const data = resp?.data;
49
+ const mentions = (resp?.mentions || data?.mentions);
50
+ const users = mentions?.users;
51
+ if (Array.isArray(users)) {
52
+ for (const u of users) {
53
+ if (u && typeof u.id === "number" && typeof u.name === "string") {
54
+ map.set(u.id, u.name);
55
+ }
56
+ }
57
+ }
58
+ return map;
59
+ }
60
+ // Flatten a richText node array into a plain-text string for display. Posts
61
+ // commonly carry an empty `content` with the real body living only in
62
+ // `richText`, so list/feed labels go blank without this. Renders text nodes
63
+ // verbatim, user mentions as `@<name>` (resolved via `mentions`, else `@<id>`),
64
+ // and link nodes as their label or URL.
65
+ export function flattenRichText(richText, mentions) {
66
+ if (!Array.isArray(richText))
67
+ return "";
68
+ const parts = [];
69
+ for (const node of richText) {
70
+ if (!node || typeof node !== "object")
71
+ continue;
72
+ const n = node;
73
+ if (typeof n.text === "string" && n.text) {
74
+ parts.push(n.text);
75
+ }
76
+ else if (n.type === "user") {
77
+ const id = n.userId;
78
+ const resolved = n.name ||
79
+ (typeof id === "number" ? mentions?.get(id) : undefined) ||
80
+ "";
81
+ parts.push(resolved ? `@${resolved.replace(/^@/, "")}` : id != null ? `@${id}` : "");
82
+ }
83
+ else if (n.type === "link") {
84
+ parts.push(n.text || n.url || n.href || "");
85
+ }
86
+ }
87
+ return parts.join("");
88
+ }
89
+ // Best available plain-text body for a post or reply: explicit `content` first,
90
+ // then flattened `richText`. Empty string if neither has text.
91
+ export function postBodyText(m, mentions) {
92
+ const content = m.content || "";
93
+ if (content.trim())
94
+ return content;
95
+ return flattenRichText(m.richText, mentions);
96
+ }
97
+ // Single-line display label for a post: its title, else a body snippet, else
98
+ // "(untitled)". Falls through title -> content -> richText so titleless posts
99
+ // (the common case) read sensibly instead of printing "null"/blank. Body
100
+ // snippets are collapsed to one line and truncated; titles are left intact.
101
+ export function formatPostLabel(m, mentions, maxLen = 100) {
102
+ const title = m.title || "";
103
+ if (title.trim())
104
+ return title;
105
+ const body = postBodyText(m, mentions).replace(/\s+/g, " ").trim();
106
+ if (!body)
107
+ return "(untitled)";
108
+ return body.length > maxLen ? body.slice(0, maxLen) + "…" : body;
109
+ }
110
+ // Compact one-line rendering of a reply for nested display under a post (used
111
+ // by `list-posts`). Indented two levels so it reads as a child of the post
112
+ // line; surfaces attachments/artifacts and reactions on the reply.
113
+ export function formatReplyLine(r, mentions, maxLen = 200) {
114
+ const author = r.author?.name ||
115
+ `User ${r.authorId ?? "?"}`;
116
+ const text = postBodyText(r, mentions).replace(/\s+/g, " ").trim();
117
+ const body = text.length > maxLen ? text.slice(0, maxLen) + "…" : text;
118
+ const attach = formatAttachmentSummary(r);
119
+ const chips = formatReactionChips(r);
120
+ return (` - [r:${r.id}] ${author}: ${body} (${r.createdAt})` +
121
+ (attach ? ` ${attach}` : "") +
122
+ (chips ? ` ${chips}` : ""));
123
+ }
41
124
  // Classify a wire attachment the way the clients do: declared mimeType first,
42
125
  // then the media-url extension (covers rows that predate the mimeType field).
43
126
  function attachmentKind(a) {
@@ -78,9 +161,12 @@ export function formatAttachmentSummary(m) {
78
161
  }
79
162
  return `📎 ${parts.join(", ")}`;
80
163
  }
81
- // One line per attachment with the fetchable URLused by `get-post` so an
82
- // agent can download/describe the media without re-querying in --json mode.
83
- export function formatAttachmentLines(m, indent = " ") {
164
+ // One line per attachment with a fetchable referencea `mediaUrl` for files
165
+ // and media, or the `artifactId` for artifacts (retrievable via
166
+ // `gobi artifact get <id>`). Used by `get-post` and nested under posts in
167
+ // `list-posts` so an agent can fetch/describe the data without re-querying.
168
+ // `marker` is the bullet prefix (e.g. "-" or "📎").
169
+ export function formatAttachmentLines(m, indent = " ", marker = "-") {
84
170
  const attachments = m.attachments || [];
85
171
  if (!Array.isArray(attachments) || !attachments.length)
86
172
  return [];
@@ -90,11 +176,11 @@ export function formatAttachmentLines(m, indent = " ") {
90
176
  const art = a.artifact;
91
177
  const title = art.title ? ` "${art.title}"` : "";
92
178
  const status = art.isPublished ? "published" : "unpublished";
93
- return `${indent}- artifact [${art.kind}]${title} (${art.artifactId}, ${status})${art.mediaUrl ? ` — ${art.mediaUrl}` : ""}`;
179
+ return `${indent}${marker} artifact [${art.kind}]${title} (${art.artifactId}, ${status})${art.mediaUrl ? ` — ${art.mediaUrl}` : ""}`;
94
180
  }
95
181
  const dims = a.width && a.height ? ` ${a.width}×${a.height}` : "";
96
182
  const name = a.fileName ? ` "${a.fileName}"` : "";
97
183
  const mime = a.mimeType ? ` (${a.mimeType})` : "";
98
- return `${indent}- ${kind}${name}${dims}${mime} — ${a.mediaUrl}`;
184
+ return `${indent}${marker} ${kind}${name}${dims}${mime} — ${a.mediaUrl}`;
99
185
  });
100
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "2.0.34",
3
+ "version": "2.0.35",
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.34"
12
+ version: "2.0.35"
13
13
  ---
14
14
 
15
15
  # gobi-artifact
16
16
 
17
- Gobi artifact commands for versioned, post-attachable creations (v2.0.34).
17
+ Gobi artifact commands for versioned, post-attachable creations (v2.0.35).
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.34"
11
+ version: "2.0.35"
12
12
  ---
13
13
 
14
14
  # gobi-core
15
15
 
16
- Core CLI commands for the Gobi collaborative knowledge platform (v2.0.34).
16
+ Core CLI commands for the Gobi collaborative knowledge platform (v2.0.35).
17
17
 
18
18
  ## Prerequisites
19
19
 
@@ -7,7 +7,7 @@ description: >-
7
7
  allowed-tools: Bash(gobi:*)
8
8
  metadata:
9
9
  author: gobi-ai
10
- version: "2.0.34"
10
+ version: "2.0.35"
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.34"
13
+ version: "2.0.35"
14
14
  ---
15
15
 
16
16
  # gobi-media
17
17
 
18
- Gobi media generation commands (v2.0.34).
18
+ Gobi media generation commands (v2.0.35).
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.34"
10
+ version: "2.0.35"
11
11
  ---
12
12
 
13
13
  # gobi-sense
14
14
 
15
- Gobi sense commands for activity and transcription data (v2.0.34).
15
+ Gobi sense commands for activity and transcription data (v2.0.35).
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.34"
14
+ version: "2.0.35"
15
15
  ---
16
16
 
17
17
  # gobi-space
18
18
 
19
- Gobi space, global, and personal-space posts (v2.0.34).
19
+ Gobi space, global, and personal-space posts (v2.0.35).
20
20
 
21
21
  Requires gobi-cli installed and authenticated. See the **gobi-core** skill for setup.
22
22
 
@@ -8,12 +8,12 @@ description: >-
8
8
  allowed-tools: Bash(gobi:*)
9
9
  metadata:
10
10
  author: gobi-ai
11
- version: "2.0.34"
11
+ version: "2.0.35"
12
12
  ---
13
13
 
14
14
  # gobi-vault
15
15
 
16
- Gobi vault commands for publishing your vault profile and syncing files (v2.0.34).
16
+ Gobi vault commands for publishing your vault profile and syncing files (v2.0.35).
17
17
 
18
18
  Requires gobi-cli installed and authenticated. See gobi-core skill for setup.
19
19