@codeforbreakfast/commy-mcp 0.11.3 → 0.14.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +153 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeforbreakfast/commy-mcp",
3
- "version": "0.11.3",
3
+ "version": "0.14.0",
4
4
  "description": "commy MCP server — the inter-agent communication substrate, runnable on node via npx.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/server.js CHANGED
@@ -47088,6 +47088,36 @@ var wrapBotHttp = (inner, resolveDirectory, selfUserId) => ({
47088
47088
  delete: (path, schema, body) => inner.delete(path, schema, body)
47089
47089
  });
47090
47090
 
47091
+ // packages/zulip/permalink.ts
47092
+ var HASH_REPLACEMENTS = {
47093
+ "%": ".",
47094
+ "!": ".21",
47095
+ "'": ".27",
47096
+ "(": ".28",
47097
+ ")": ".29",
47098
+ "*": ".2A",
47099
+ ".": ".2E"
47100
+ };
47101
+ var encodeHashComponent = (value5) => encodeURIComponent(value5).replace(/[%!'()*.]/g, (matched) => HASH_REPLACEMENTS[matched] ?? matched);
47102
+ var permalinkBase = (config2) => config2.hostHeader === undefined ? config2.realmUrl.replace(/\/+$/, "") : `https://${config2.hostHeader}`;
47103
+ var channelSlug = (channel) => `${channel.id}-${encodeHashComponent(channel.name.replaceAll(" ", "-"))}`;
47104
+ var channelPermalink = (base, channel) => `${base}/#narrow/channel/${channelSlug(channel)}`;
47105
+ var topicPermalink = (base, channel, topic) => `${channelPermalink(base, channel)}/topic/${encodeHashComponent(topic)}`;
47106
+ var messagePermalink = (base, channel, id3, topic) => topic === undefined ? `${channelPermalink(base, channel)}/near/${id3}` : `${topicPermalink(base, channel, topic)}/near/${id3}`;
47107
+ var withChannelPermalink = (base, channel) => ({ ...channel, permalink: channelPermalink(base, channel) });
47108
+ var buildMessageRef = (base, id3, channel, threadName3) => {
47109
+ const decoratedChannel = withChannelPermalink(base, channel);
47110
+ return threadName3 === undefined ? { id: id3, channel: decoratedChannel, permalink: messagePermalink(base, decoratedChannel, id3) } : {
47111
+ id: id3,
47112
+ channel: decoratedChannel,
47113
+ thread: {
47114
+ name: threadName3,
47115
+ permalink: topicPermalink(base, decoratedChannel, threadName3)
47116
+ },
47117
+ permalink: messagePermalink(base, decoratedChannel, id3, threadName3)
47118
+ };
47119
+ };
47120
+
47091
47121
  // packages/zulip/events.ts
47092
47122
  var createMessageRefCache = (maxSize = 1e4) => {
47093
47123
  const map32 = new Map;
@@ -47177,16 +47207,12 @@ var extractMentions = (content, byName) => {
47177
47207
  }
47178
47208
  return results;
47179
47209
  };
47180
- var decodeMessageRef = (message) => exports_Effect.gen(function* () {
47210
+ var decodeMessageRef = (message, base) => exports_Effect.gen(function* () {
47181
47211
  const id3 = yield* decodeMessageId(String(message.id));
47182
47212
  const channelId = yield* decodeChannelId(String(message.stream_id));
47183
47213
  const channelName = yield* decodeChannelName(message.display_recipient);
47184
47214
  const threadName3 = yield* decodeThreadName(message.subject);
47185
- return {
47186
- id: id3,
47187
- channel: { id: channelId, name: channelName },
47188
- thread: { name: threadName3 }
47189
- };
47215
+ return buildMessageRef(base, id3, { id: channelId, name: channelName }, threadName3);
47190
47216
  });
47191
47217
  var decodeSenderIdentity = (message, directory) => {
47192
47218
  const cached4 = directory.byId.get(message.sender_id);
@@ -47198,9 +47224,9 @@ var decodeSenderIdentity = (message, directory) => {
47198
47224
  return { id: id3, name, kind: "human" };
47199
47225
  });
47200
47226
  };
47201
- var messageToInboundEvents = (message, directory, boundIdentity) => exports_Effect.gen(function* () {
47227
+ var messageToInboundEvents = (message, directory, boundIdentity, base) => exports_Effect.gen(function* () {
47202
47228
  const sender = yield* decodeSenderIdentity(message, directory);
47203
- const ref = yield* decodeMessageRef(message);
47229
+ const ref = yield* decodeMessageRef(message, base);
47204
47230
  const body = yield* decodeMessageBody(message.content);
47205
47231
  const ts = yield* decodeTimestamp(message.timestamp);
47206
47232
  const portMessage = {
@@ -47223,12 +47249,12 @@ var messageToInboundEvents = (message, directory, boundIdentity) => exports_Effe
47223
47249
  });
47224
47250
  var channelShapeSchema = exports_Schema.Struct({ stream_id: exports_Schema.Number });
47225
47251
  var decodeChannelShape = exports_Schema.decodeUnknownEither(channelShapeSchema);
47226
- var mapMessageEvent = (raw3, directory, boundIdentity) => {
47252
+ var mapMessageEvent = (raw3, directory, boundIdentity, base) => {
47227
47253
  const message = raw3["message"];
47228
47254
  if (exports_Either.isLeft(decodeChannelShape(message))) {
47229
47255
  return exports_Effect.succeed([]);
47230
47256
  }
47231
- return decodeZulipMessageContent(message).pipe(exports_Effect.flatMap((parsed) => messageToInboundEvents(parsed, directory, boundIdentity)));
47257
+ return decodeZulipMessageContent(message).pipe(exports_Effect.flatMap((parsed) => messageToInboundEvents(parsed, directory, boundIdentity, base)));
47232
47258
  };
47233
47259
  var reactionKind = (op) => op === "add" ? "reaction-added" : "reaction-removed";
47234
47260
  var decodeReactionIdentity = (reaction, directory) => {
@@ -47267,7 +47293,7 @@ var singleMessageResponseSchema = exports_Schema.Struct({
47267
47293
  result: exports_Schema.Literal("success"),
47268
47294
  messages: exports_Schema.Array(zulipMessageContentSchema)
47269
47295
  });
47270
- var fetchMessageRef = (http, messageId) => http.get("/messages", singleMessageResponseSchema, {
47296
+ var fetchMessageRef = (http, messageId, base) => http.get("/messages", singleMessageResponseSchema, {
47271
47297
  anchor: messageId,
47272
47298
  num_before: 0,
47273
47299
  num_after: 0,
@@ -47277,7 +47303,7 @@ var fetchMessageRef = (http, messageId) => http.get("/messages", singleMessageRe
47277
47303
  const message = res.messages[0];
47278
47304
  if (message === undefined || message.id !== messageId)
47279
47305
  return exports_Effect.succeed(exports_Option.none());
47280
- return decodeMessageRef(message).pipe(exports_Effect.map(exports_Option.some));
47306
+ return decodeMessageRef(message, base).pipe(exports_Effect.map(exports_Option.some));
47281
47307
  }));
47282
47308
  var describeError = (err) => messageOf(err);
47283
47309
  var markReplayed = (events) => events.map((ev) => {
@@ -47290,7 +47316,7 @@ var defaultRetrySchedule = exports_Schedule.exponential(exports_Duration.seconds
47290
47316
  var skipWithLog = (line) => exports_Effect.logInfo(line).pipe(exports_Effect.as([]));
47291
47317
  var processSingleEvent = (evt, directory, config2) => {
47292
47318
  if (evt.type === "message") {
47293
- return mapMessageEvent(evt, directory, config2.boundIdentity).pipe(exports_Effect.tap((events) => exports_Effect.sync(() => {
47319
+ return mapMessageEvent(evt, directory, config2.boundIdentity, config2.permalinkBase).pipe(exports_Effect.tap((events) => exports_Effect.sync(() => {
47294
47320
  if (config2.messageRefCache !== undefined) {
47295
47321
  populateCacheFromInboundEvents(config2.messageRefCache, events);
47296
47322
  }
@@ -47308,7 +47334,7 @@ var processSingleEvent = (evt, directory, config2) => {
47308
47334
  if (cachedTarget !== undefined) {
47309
47335
  return eventsForTarget(cachedTarget).pipe(exports_Effect.catchTag("ParseError", (err) => skipWithLog(`commy zulip events: skipping reaction event id=${evt.id}: ${describeError(err)}`)));
47310
47336
  }
47311
- return fetchMessageRef(config2.http, reaction.message_id).pipe(exports_Effect.flatMap(exports_Option.match({
47337
+ return fetchMessageRef(config2.http, reaction.message_id, config2.permalinkBase).pipe(exports_Effect.flatMap(exports_Option.match({
47312
47338
  onNone: () => exports_Effect.succeed([]),
47313
47339
  onSome: (target) => {
47314
47340
  cache.set(target.id, target);
@@ -47554,6 +47580,12 @@ class ReactivateForbidden extends exports_Data.TaggedError("ReactivateForbidden"
47554
47580
  return `cannot reactivate deactivated bot ${this.userId}: the minter is not an ` + `organization administrator, and POST /users/{id}/reactivate is admin-only. ` + `Pin this identity with COMMY_BOT_NAME so it is never deactivated on ` + `release, or grant the minter admin rights. Underlying realm ` + `error: ${this.cause.message}`;
47555
47581
  }
47556
47582
  }
47583
+
47584
+ class AttachIdentityNotFound extends exports_Data.TaggedError("AttachIdentityNotFound") {
47585
+ get message() {
47586
+ return `attach mode: no provisioned bot named '${this.name}' on the realm. ` + `Provision the persona once and supply its stable api key before attaching.`;
47587
+ }
47588
+ }
47557
47589
  var SHORT_NAME_PATTERN = /[^a-z0-9._-]/g;
47558
47590
  var sanitiseShortName = (name) => {
47559
47591
  const collapsed = name.toLowerCase().replace(SHORT_NAME_PATTERN, "-");
@@ -47741,7 +47773,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47741
47773
  const body = yield* decodeMessageBody(m.content);
47742
47774
  const reactions = yield* mapHistoricalReactions(m.reactions, directory2);
47743
47775
  return {
47744
- ref: { id: m.id, channel, thread: { name: m.subject } },
47776
+ ref: decorateMessageRef(m.id, channel, m.subject),
47745
47777
  sender,
47746
47778
  body,
47747
47779
  ts: m.ts,
@@ -47760,6 +47792,9 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47760
47792
  })
47761
47793
  ], { concurrency: 2 }).pipe(exports_Effect.flatMap(([directory2, res]) => exports_Effect.forEach(res.messages, toHistoricalMessage).pipe(exports_Effect.map((historical) => historical.filter(inRange(range5))), exports_Effect.flatMap((inRangeMessages) => exports_Effect.forEach(inRangeMessages, (m) => mapHistoricalMessage(m, channel, directory2))))));
47762
47794
  const realmHost = new URL(config2.realmUrl).hostname;
47795
+ const base = permalinkBase(config2);
47796
+ const decorateChannel = (channel) => withChannelPermalink(base, channel);
47797
+ const decorateMessageRef = (id3, channel, threadName3) => buildMessageRef(base, id3, channel, threadName3);
47763
47798
  const buildBotEmail = (shortName) => BotEmail(`${shortName}-bot@${realmHost}`);
47764
47799
  const findAnyBotByName = (name) => minterHttp.get("/users", usersSchema).pipe(exports_Effect.map((res) => exports_Option.fromNullable(res.members.find((u) => u.full_name === name && u.is_bot))));
47765
47800
  const mintBot = (name) => {
@@ -47793,6 +47828,18 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47793
47828
  return reactivate.pipe(exports_Effect.flatMap(() => regenerateBotKey(existing)));
47794
47829
  }
47795
47830
  })));
47831
+ const attachBot = (name, apiKey) => findAnyBotByName(name).pipe(exports_Effect.flatMap(exports_Option.match({
47832
+ onNone: () => exports_Effect.fail(new AttachIdentityNotFound({ name })),
47833
+ onSome: (existing) => BotEmail(existing.email).pipe(exports_Effect.map((email) => ({
47834
+ userId: ZulipUserRef(existing.user_id),
47835
+ apiKey,
47836
+ email
47837
+ })))
47838
+ })));
47839
+ const provideMintedFor = (name) => {
47840
+ const attach = config2.attachIdentity;
47841
+ return attach !== undefined && attach.name === name ? attachBot(name, exports_Redacted.value(attach.apiKey)) : acquireBot(name);
47842
+ };
47796
47843
  const identity6 = {
47797
47844
  currentIdentity: () => requireBound().pipe(exports_Effect.map((b) => b.identity)),
47798
47845
  acquire: (name) => exports_SynchronizedRef.modifyEffect(boundRef, (current2) => exports_Option.match(current2, {
@@ -47805,7 +47852,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47805
47852
  }
47806
47853
  return exports_Effect.die(new Error(`zulipAdapter: already bound to ${existing.acquiredName} — release() before acquiring ${name}`));
47807
47854
  },
47808
- onNone: () => acquireBot(name).pipe(exports_Effect.flatMap((minted) => exports_Effect.gen(function* () {
47855
+ onNone: () => provideMintedFor(name).pipe(exports_Effect.flatMap((minted) => exports_Effect.gen(function* () {
47809
47856
  const innerHttp = yield* makeZulipHttp(buildHttpConfig(config2.realmUrl, minted.email, minted.apiKey, config2.hostHeader)).pipe(exports_Effect.provideService(exports_HttpClient.HttpClient, httpClient));
47810
47857
  const http = wrapBotHttp(innerHttp, recipientDirectory, minted.userId);
47811
47858
  const ident = {
@@ -47891,10 +47938,10 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47891
47938
  }).pipe(exports_Effect.flatMap((res) => exports_Effect.gen(function* () {
47892
47939
  const map32 = new Map;
47893
47940
  for (const s of res.streams) {
47894
- map32.set(s.name, {
47941
+ map32.set(s.name, decorateChannel({
47895
47942
  id: yield* decodeChannelId(String(s.stream_id)),
47896
47943
  name: yield* decodeChannelName(s.name)
47897
- });
47944
+ }));
47898
47945
  }
47899
47946
  return map32;
47900
47947
  })));
@@ -47916,7 +47963,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
47916
47963
  to: effective.name,
47917
47964
  content: body,
47918
47965
  topic: thread === undefined ? "(no topic)" : thread.name
47919
- }).pipe(exports_Effect.flatMap((sent) => decodeMessageId(String(sent.id)).pipe(exports_Effect.map((id3) => thread === undefined ? { id: id3, channel: effective } : { id: id3, channel: effective, thread }))))))), exports_Effect.mapError((cause3) => cause3 instanceof UnknownChannel ? cause3 : new PublisherError({ operation: "post", cause: cause3 })));
47966
+ }).pipe(exports_Effect.flatMap((sent) => decodeMessageId(String(sent.id)).pipe(exports_Effect.map((id3) => decorateMessageRef(id3, effective, thread?.name)))))))), exports_Effect.mapError((cause3) => cause3 instanceof UnknownChannel ? cause3 : new PublisherError({ operation: "post", cause: cause3 })));
47920
47967
  },
47921
47968
  edit: (message, body) => boundHttp().pipe(exports_Effect.flatMap((http) => http.patch(`/messages/${encodeURIComponent(message.id)}`, successSchema2, {
47922
47969
  content: body
@@ -48018,6 +48065,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
48018
48065
  }).pipe(exports_Effect.mapError((cause3) => new InboxError({ operation: "unsubscribe", cause: cause3 }))),
48019
48066
  events: () => exports_Stream.unwrap(exports_Effect.all([exports_SynchronizedRef.get(boundRef), exports_Ref.get(inboxRef)]).pipe(exports_Effect.map(([current2, state]) => inboxEvents({
48020
48067
  http: minterHttp,
48068
+ permalinkBase: base,
48021
48069
  resolveDirectory: buildDirectoryLookup,
48022
48070
  mode: currentMode(state),
48023
48071
  messageRefCache,
@@ -48046,7 +48094,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
48046
48094
  exports_SynchronizedRef.get(boundRef)
48047
48095
  ], { concurrency: 2 }).pipe(exports_Effect.flatMap(([directory2, res, current2]) => exports_Effect.forEach(res.messages.filter((raw3) => raw3.timestamp >= since), (raw3) => {
48048
48096
  const { flags: _flags, ...message } = raw3;
48049
- return messageToInboundEvents(message, directory2, exports_Option.getOrUndefined(current2)?.identity);
48097
+ return messageToInboundEvents(message, directory2, exports_Option.getOrUndefined(current2)?.identity, base);
48050
48098
  }).pipe(exports_Effect.map((perMessage) => {
48051
48099
  const out = [];
48052
48100
  for (const mapped of perMessage) {
@@ -48102,7 +48150,8 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
48102
48150
  return exports_Array.sort(threads, exports_Order.reverse(exports_Order.mapInput(exports_Order.number, (t) => t.lastPostTs)));
48103
48151
  })));
48104
48152
  }), exports_Effect.mapError((cause3) => new HistoryError({ operation: "recentThreads", cause: cause3 })));
48105
- }
48153
+ },
48154
+ messagePermalink: (id3, hint) => (hint === undefined ? fetchMessageRef(minterHttp, Number(id3), base).pipe(exports_Effect.map(exports_Option.flatMap((ref) => exports_Option.fromNullable(ref.permalink)))) : lookupChannel(hint.channel).pipe(exports_Effect.map(exports_Option.flatMap((channel) => exports_Option.fromNullable(buildMessageRef(base, id3, channel, hint.thread).permalink))))).pipe(exports_Effect.mapError((cause3) => new HistoryError({ operation: "messagePermalink", cause: cause3 })))
48106
48155
  };
48107
48156
  const reconcileMinter = () => reconcileMinterSubscriptions({
48108
48157
  listUnsubscribedPublicStreams: () => minterHttp.get("/streams", streamsListResponseSchema, {
@@ -55031,22 +55080,37 @@ var optionalBotName = (key) => optionalNonEmpty(key).pipe(exports_Config.mapOrFa
55031
55080
  onSome: (botName) => exports_Either.right(exports_Option.some(botName))
55032
55081
  })
55033
55082
  })));
55083
+ var optionalApiKey = (key) => {
55084
+ const decode7 = exports_ParseResult.decodeUnknownEither(apiKeySchema);
55085
+ return optionalNonEmpty(key).pipe(exports_Config.mapOrFail((option7) => exports_Option.match(option7, {
55086
+ onNone: () => exports_Either.right(exports_Option.none()),
55087
+ onSome: (raw3) => decode7(raw3).pipe(exports_Either.mapBoth({
55088
+ onLeft: (issue2) => exports_ConfigError.InvalidData([key], exports_ParseResult.TreeFormatter.formatIssueSync(issue2)),
55089
+ onRight: (value5) => exports_Option.some(exports_Redacted.make(value5))
55090
+ }))
55091
+ })));
55092
+ };
55034
55093
  var envConfig = exports_Config.all({
55035
55094
  realmUrl: requiredBrand("ZULIP_SITE", realmUrlSchema, (v) => v),
55036
55095
  minterEmail: requiredBrand("ZULIP_MINTER_EMAIL", botEmailSchema, (v) => v),
55037
55096
  minterApiKey: exports_Config.redacted(requiredBrand("ZULIP_MINTER_API_KEY", apiKeySchema, (v) => v)),
55038
55097
  botName: optionalBotName("COMMY_BOT_NAME"),
55098
+ botApiKey: optionalApiKey("COMMY_BOT_API_KEY"),
55039
55099
  subscribe: optionalUserConfig("COMMY_SUBSCRIBE"),
55040
55100
  project: optionalNonEmpty("COMMY_PROJECT"),
55041
55101
  catchupWindowSeconds: optionalNonNegativeInt("COMMY_CATCHUP_WINDOW_SECONDS"),
55042
55102
  sessionId: optionalSessionId("CLAUDE_CODE_SESSION_ID")
55043
- }).pipe(exports_Config.map((raw3) => {
55103
+ }).pipe(exports_Config.mapOrFail((raw3) => {
55104
+ if (exports_Option.isSome(raw3.botApiKey) && exports_Option.isNone(raw3.botName)) {
55105
+ return exports_Either.left(exports_ConfigError.InvalidData(["COMMY_BOT_API_KEY"], "COMMY_BOT_API_KEY requires COMMY_BOT_NAME — the key identifies which provisioned persona to attach to"));
55106
+ }
55044
55107
  const project3 = exports_Option.flatMap(raw3.project, sanitiseProjectSlug);
55045
- return {
55108
+ return exports_Either.right({
55046
55109
  realmUrl: raw3.realmUrl,
55047
55110
  minterEmail: raw3.minterEmail,
55048
55111
  minterApiKey: raw3.minterApiKey,
55049
55112
  ...exports_Option.match(raw3.botName, { onNone: () => ({}), onSome: (botName) => ({ botName }) }),
55113
+ ...exports_Option.isSome(raw3.botName) && exports_Option.isSome(raw3.botApiKey) ? { attachIdentity: { name: raw3.botName.value, apiKey: raw3.botApiKey.value } } : {},
55050
55114
  ...exports_Option.match(raw3.subscribe, {
55051
55115
  onNone: () => ({}),
55052
55116
  onSome: (subscribe4) => ({ subscribe: subscribe4 })
@@ -55060,7 +55124,7 @@ var envConfig = exports_Config.all({
55060
55124
  onNone: () => ({}),
55061
55125
  onSome: (catchupWindowSeconds) => ({ catchupWindowSeconds })
55062
55126
  })
55063
- };
55127
+ });
55064
55128
  }));
55065
55129
  var parseEnv = envConfig.pipe(exports_Effect.mapError((error2) => envConfigError(renderConfigError(error2))));
55066
55130
  var sanitiseProjectSlug = (raw3) => {
@@ -55121,7 +55185,8 @@ var ZulipAdapterLive = substrateAdapterLayer(exports_Effect.gen(function* () {
55121
55185
  return yield* zulipAdapter({
55122
55186
  realmUrl: parsed.realmUrl,
55123
55187
  minterEmail: parsed.minterEmail,
55124
- minterApiKey: parsed.minterApiKey
55188
+ minterApiKey: parsed.minterApiKey,
55189
+ ...parsed.attachIdentity === undefined ? {} : { attachIdentity: parsed.attachIdentity }
55125
55190
  });
55126
55191
  }));
55127
55192
  var subscribeFromEnv = (inbox, narrowSet, parsed) => {
@@ -55146,6 +55211,9 @@ var formatMessage = (event, botIdentityId) => {
55146
55211
  ["channel_name", ref.channel.name],
55147
55212
  ["thread", ref.thread?.name],
55148
55213
  ["message_id", ref.id],
55214
+ ["permalink", ref.permalink],
55215
+ ["channel_permalink", ref.channel.permalink],
55216
+ ["thread_permalink", ref.thread?.permalink],
55149
55217
  ["sender_id", sender.id],
55150
55218
  ["sender_name", sender.name],
55151
55219
  ["sender_kind", sender.kind],
@@ -55164,6 +55232,7 @@ var formatReaction = (event, observedAt) => {
55164
55232
  ["target_message_id", event.target.id],
55165
55233
  ["target_channel_name", event.target.channel.name],
55166
55234
  ["target_thread", event.target.thread?.name],
55235
+ ["target_permalink", event.target.permalink],
55167
55236
  ["reaction_emoji", event.emoji],
55168
55237
  ["reaction_action", action],
55169
55238
  ["by_id", event.by.id],
@@ -56898,7 +56967,7 @@ class Server extends Protocol {
56898
56967
  }
56899
56968
 
56900
56969
  // packages/mcp/mcp-server.ts
56901
- var PLUGIN_VERSION = "0.11.3";
56970
+ var PLUGIN_VERSION = "0.14.0";
56902
56971
  var COMMY_INSTRUCTIONS = `**Canonical substrate.** commy is the inter-agent channel on this workstation. Where you also see \`claude-peers\`, \`agent-mail\`, or Discord MCP tools, prefer commy — claude-peers and Discord are retiring (comms-sxf); agent-mail is a mail-shaped substrate kept for specific flows. Don't fan the same message across substrates.
56903
56972
 
56904
56973
  **Channels.** Each project has one channel: \`#<project-slug>\` where the slug is resolved per session as \`COMMY_PROJECT\` env > git remote \`origin\` basename > git root basename. Sessions launched outside a git repo (e.g. \`/tmp\`, \`$HOME\`) have no project channel and should post to \`#general\` instead. Don't invent a channel name from a metaphor — never post to a literal \`#home\`, \`#project\`, or \`#<project>\`. Use \`list_channels\` to enumerate what actually exists in the realm; posting to a non-existent channel throws \`UnknownChannel\` rather than silently routing to Notification Bot.
@@ -56907,7 +56976,9 @@ var COMMY_INSTRUCTIONS = `**Canonical substrate.** commy is the inter-agent chan
56907
56976
 
56908
56977
  **Subscriptions.** Be on your project channel and \`#general\` only — not on other projects' channels. The workstation's boot-time defaults come from the plugin's \`COMMY_SUBSCRIBE\` userConfig (tokens: \`channel:<name>\`, \`thread:<channel>/<thread>\`, \`new-topics:<channel>\`, \`mentions\`); adjust the live set via \`subscribe\`/\`unsubscribe\` at runtime. Inbound matches arrive as \`<channel source="commy" ...>\` blocks. If one arrives from a channel you aren't subscribed to, treat it as background context and don't reply — only post into your project channel, into \`#general\`, or into threads you've explicitly joined. **Refer to peers by name** — the \`sender_name\` (and reaction \`by_name\`) in those blocks — never by a bare number; reserve numbers for message and bead ids, which otherwise collide with identity in a thread.
56909
56978
 
56910
- **Tools.** \`post\` (channel; optionally thread or reply), \`react\`/\`unreact\` (emoji on a message), \`subscribe\`/\`unsubscribe\` (live target), \`read_channel\`/\`read_thread\` (history within a range), \`list_channels\` (enumerate channels in the realm), \`resolve\` (identity by name), \`current_identity\` (passive — never acquires), \`download_file\` (fetch a \`/user_uploads/...\` attachment to a temp file — use Read on the returned path to view images), \`upload_file\` (upload a local file by absolute path; returns a \`reference\` string to embed in a \`post\` body).
56979
+ **Links.** Every ref the substrate hands you carries a ready-to-click \`permalink\` — on \`post\` results, \`read_channel\`/\`read_thread\` messages (message \`permalink\` plus \`channel.permalink\` and \`thread.permalink\`), \`list_channels\` (channel \`permalink\`), and inbound \`<channel source="commy">\` frames (\`permalink\` / \`channel_permalink\` / \`thread_permalink\` meta, \`target_permalink\` on reaction frames). **Whenever you show a human a message, channel, or topic reference, render it as that clickable permalink — never a bare name or numeric id.** A human can click a permalink straight to the message; a bare \`#channel > topic\` or message number makes them hunt. When you hold only a message id (e.g. one cited elsewhere, with no permalink to hand), \`message_link(message_id, channel_name?, thread?)\` returns its \`{permalink}\`.
56980
+
56981
+ **Tools.** \`post\` (channel; optionally thread or reply), \`react\`/\`unreact\` (emoji on a message), \`subscribe\`/\`unsubscribe\` (live target), \`read_channel\`/\`read_thread\` (history within a range), \`list_channels\` (enumerate channels in the realm), \`message_link\` (canonical permalink for a message id), \`resolve\` (identity by name), \`current_identity\` (passive — never acquires), \`download_file\` (fetch a \`/user_uploads/...\` attachment to a temp file — use Read on the returned path to view images), \`upload_file\` (upload a local file by absolute path; returns a \`reference\` string to embed in a \`post\` body).
56911
56982
 
56912
56983
  **session_id.** Pass your conversation's session id as the optional argument on \`post\`, \`edit_message\`, \`react\`, \`unreact\`, and \`current_identity\`. **Must be a UUID** (e.g. \`crypto.randomUUID()\`); anything else is rejected as malformed and the call routes to the "missing session_id" error rather than silently minting a \`cc-<garbage>\` identity (comms-uqf). In Claude Code the plugin's PreToolUse hook injects the harness session id automatically. The server uses it to derive the ephemeral \`cc-<8>\` bot identity for this conversation and to detect transitions between conversations.`;
56913
56984
  var buildMcpServer = () => new Server({ name: "commy", version: PLUGIN_VERSION }, {
@@ -57001,7 +57072,8 @@ var identityShape = (identity6) => ({
57001
57072
  });
57002
57073
  var channelShape = (channel) => ({
57003
57074
  id: channel.id,
57004
- name: channel.name
57075
+ name: channel.name,
57076
+ permalink: channel.permalink ?? null
57005
57077
  });
57006
57078
  var createCache = () => {
57007
57079
  const identityById = new Map;
@@ -57058,8 +57130,13 @@ var resolveMentions = (cache, mentionIds) => exports_Effect.forEach(mentionIds,
57058
57130
  }));
57059
57131
  var messageShape = (m) => ({
57060
57132
  id: m.ref.id,
57061
- channel: { id: m.ref.channel.id, name: m.ref.channel.name },
57062
- thread: m.ref.thread === undefined ? null : { name: m.ref.thread.name },
57133
+ channel: {
57134
+ id: m.ref.channel.id,
57135
+ name: m.ref.channel.name,
57136
+ permalink: m.ref.channel.permalink ?? null
57137
+ },
57138
+ thread: m.ref.thread === undefined ? null : { name: m.ref.thread.name, permalink: m.ref.thread.permalink ?? null },
57139
+ permalink: m.ref.permalink ?? null,
57063
57140
  sender: identityShape(m.sender),
57064
57141
  body: m.body,
57065
57142
  ts: m.ts,
@@ -57124,6 +57201,11 @@ var ReadThreadArgs = exports_Schema.Struct({
57124
57201
  thread: exports_Schema.String,
57125
57202
  ...RangeArgs
57126
57203
  });
57204
+ var MessageLinkArgs = exports_Schema.Struct({
57205
+ message_id: exports_Schema.String,
57206
+ channel_name: exports_Schema.optional(exports_Schema.String),
57207
+ thread: exports_Schema.optional(exports_Schema.String)
57208
+ });
57127
57209
  var PresenceArgs = exports_Schema.Struct({ identity_id: exports_Schema.String });
57128
57210
  var DownloadFileArgs = exports_Schema.Struct({ url_path: exports_Schema.String });
57129
57211
  var UploadFileArgs = exports_Schema.Struct({ path: exports_Schema.String });
@@ -57235,7 +57317,7 @@ var buildToolDefs = (deps, cache) => {
57235
57317
  },
57236
57318
  {
57237
57319
  name: "list_channels",
57238
- description: "List every channel the substrate is aware of, returning {id, name} for each. Use this for discovery instead of guessing channel names — posting to a non-existent channel throws UnknownChannel.",
57320
+ description: "List every channel the substrate is aware of, returning {id, name, permalink} for each. Use this for discovery instead of guessing channel names — posting to a non-existent channel throws UnknownChannel. When you show a human a channel, link it by its permalink rather than a bare name.",
57239
57321
  inputSchema: emptyObjectSchema,
57240
57322
  handler: async () => {
57241
57323
  const channels = await runEdge(adapter4.directory.listChannels());
@@ -57246,7 +57328,7 @@ var buildToolDefs = (deps, cache) => {
57246
57328
  },
57247
57329
  {
57248
57330
  name: "post",
57249
- description: "Post a message to a channel by name. Optional thread (topic), mentions (identity ids the bot has seen), and reply_to (message id). To ping someone on Zulip, write the @**Name** markup inline in body where you want it rendered — the mentions array is notification metadata only and does NOT modify body. Returns {message_id, channel_id, channel_name, thread}.",
57331
+ description: "Post a message to a channel by name. Optional thread (topic), mentions (identity ids the bot has seen), and reply_to (message id). To ping someone on Zulip, write the @**Name** markup inline in body where you want it rendered — the mentions array is notification metadata only and does NOT modify body. Returns {message_id, channel_id, channel_name, thread, permalink}. When you show a human this message afterwards, link it by the returned permalink — never a bare name or message id.",
57250
57332
  inputSchema: {
57251
57333
  type: "object",
57252
57334
  properties: {
@@ -57296,7 +57378,8 @@ var buildToolDefs = (deps, cache) => {
57296
57378
  message_id: ref.id,
57297
57379
  channel_id: ref.channel.id,
57298
57380
  channel_name: ref.channel.name,
57299
- thread: ref.thread === undefined ? null : { name: ref.thread.name }
57381
+ thread: ref.thread === undefined ? null : { name: ref.thread.name },
57382
+ permalink: ref.permalink ?? null
57300
57383
  };
57301
57384
  }
57302
57385
  },
@@ -57450,7 +57533,7 @@ var buildToolDefs = (deps, cache) => {
57450
57533
  },
57451
57534
  {
57452
57535
  name: "read_channel",
57453
- description: "Read recent messages from a channel by name. Returns {messages: Message[]} bounded by optional since/until/limit.",
57536
+ description: "Read recent messages from a channel by name. Returns {messages: Message[]} bounded by optional since/until/limit. Each message carries a clickable permalink (and channel.permalink / thread.permalink) — when you cite one of these to a human, render it as that permalink, not a bare name or id.",
57454
57537
  inputSchema: {
57455
57538
  type: "object",
57456
57539
  properties: {
@@ -57475,7 +57558,7 @@ var buildToolDefs = (deps, cache) => {
57475
57558
  },
57476
57559
  {
57477
57560
  name: "read_thread",
57478
- description: "Read recent messages from a thread (topic) within a channel. Returns {messages: Message[]} bounded by optional since/until/limit.",
57561
+ description: "Read recent messages from a thread (topic) within a channel. Returns {messages: Message[]} bounded by optional since/until/limit. Each message carries a clickable permalink (and channel.permalink / thread.permalink) — when you cite one of these to a human, render it as that permalink, not a bare name or id.",
57479
57562
  inputSchema: {
57480
57563
  type: "object",
57481
57564
  properties: {
@@ -57499,6 +57582,41 @@ var buildToolDefs = (deps, cache) => {
57499
57582
  return { messages: messages.map(messageShape) };
57500
57583
  }
57501
57584
  },
57585
+ {
57586
+ name: "message_link",
57587
+ description: "Return the canonical clickable permalink for a message id: {permalink}. Use when you hold only an id (e.g. one cited elsewhere) and want the URL without re-deriving the narrow format — messages from post/read already carry a permalink. A message just posted or read is resolved from cache; otherwise pass channel_name (and thread) to build the link directly, or omit them to look the message up by id. Returns {permalink: null} when the message cannot be resolved.",
57588
+ inputSchema: {
57589
+ type: "object",
57590
+ properties: {
57591
+ message_id: { type: "string", description: "Message id to link to" },
57592
+ channel_name: {
57593
+ type: "string",
57594
+ description: "Channel the message lives in (lets the link be built without a lookup)"
57595
+ },
57596
+ thread: {
57597
+ type: "string",
57598
+ description: "Thread / topic the message lives in (sharpens the link to the topic)"
57599
+ }
57600
+ },
57601
+ required: ["message_id"],
57602
+ additionalProperties: false
57603
+ },
57604
+ handler: async (args2) => {
57605
+ const permalink = await runEdge(exports_Effect.gen(function* () {
57606
+ const parsed = yield* exports_Schema.decodeUnknown(MessageLinkArgs)(args2);
57607
+ const id3 = yield* decodeMessageId(parsed.message_id);
57608
+ const cached5 = cache.messageById.get(id3);
57609
+ if (cached5?.permalink !== undefined)
57610
+ return cached5.permalink;
57611
+ const hint = parsed.channel_name === undefined ? undefined : {
57612
+ channel: yield* decodeChannelName(parsed.channel_name),
57613
+ ...parsed.thread === undefined ? {} : { thread: yield* decodeThreadName(parsed.thread) }
57614
+ };
57615
+ return exports_Option.getOrNull(yield* adapter4.history.messagePermalink(id3, hint));
57616
+ }));
57617
+ return { permalink };
57618
+ }
57619
+ },
57502
57620
  {
57503
57621
  name: "presence",
57504
57622
  description: "Return presence for the given identity id: online/idle/offline for humans, or 'unknown' for agents (bots have no presence concept — Zulip presence is human-only). Identity must already be cached via list_agents, list_humans, resolve, or current_identity — call one of those first when given a name.",