@codeforbreakfast/commy-mcp 0.17.0 → 0.19.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 +189 -98
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeforbreakfast/commy-mcp",
3
- "version": "0.17.0",
3
+ "version": "0.19.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
@@ -47638,9 +47638,11 @@ var reactionToInboundEvent = (reaction, directory, target) => exports_Effect.gen
47638
47638
  by
47639
47639
  };
47640
47640
  });
47641
- var registerQueue = (http, mode) => {
47641
+ var MAX_QUEUE_TIMEOUT_SECS = 604800;
47642
+ var registerQueue = (http, mode, idleTimeoutSecs) => {
47642
47643
  const baseParams = { event_types: JSON.stringify(["message", "reaction"]) };
47643
- const params = mode === "mentions" ? { ...baseParams, narrow: JSON.stringify([["is", "mentioned"]]) } : baseParams;
47644
+ const modeParams = mode === "mentions" ? { ...baseParams, narrow: JSON.stringify([["is", "mentioned"]]) } : baseParams;
47645
+ const params = idleTimeoutSecs === undefined ? modeParams : { ...modeParams, idle_queue_timeout: idleTimeoutSecs };
47644
47646
  return http.post("/register", registerResponseSchema, params).pipe(exports_Effect.map((res) => ({ queueId: res.queue_id, lastEventId: res.last_event_id })));
47645
47647
  };
47646
47648
  var populateCacheFromInboundEvents = (cache, events) => {
@@ -47711,6 +47713,13 @@ var inboxEvents = (config2) => exports_Stream.unwrap(exports_Effect.gen(function
47711
47713
  consecutiveFailures: 0,
47712
47714
  needsBreadcrumb: false
47713
47715
  });
47716
+ const resumeReported = yield* exports_Ref.make(false);
47717
+ const reportResume = (queueReplayed) => {
47718
+ const onResumeOutcome = config2.onResumeOutcome;
47719
+ if (onResumeOutcome === undefined)
47720
+ return exports_Effect.void;
47721
+ return exports_Ref.getAndSet(resumeReported, true).pipe(exports_Effect.flatMap((already) => already ? exports_Effect.void : onResumeOutcome(queueReplayed)));
47722
+ };
47714
47723
  const recordTransientFailure = (cause3) => exports_Ref.modify(breadcrumb, (s) => {
47715
47724
  const consecutiveFailures = s.consecutiveFailures + 1;
47716
47725
  return [consecutiveFailures, { consecutiveFailures, needsBreadcrumb: true }];
@@ -47723,6 +47732,7 @@ var inboxEvents = (config2) => exports_Stream.unwrap(exports_Effect.gen(function
47723
47732
  }
47724
47733
  })));
47725
47734
  const handleBadQueue = exports_Effect.gen(function* () {
47735
+ yield* reportResume(false);
47726
47736
  const since = yield* watermark.get();
47727
47737
  if (config2.replay === undefined || exports_Option.isNone(since)) {
47728
47738
  return [[], undefined];
@@ -47736,11 +47746,12 @@ var inboxEvents = (config2) => exports_Stream.unwrap(exports_Effect.gen(function
47736
47746
  return [markReplayed(gapEither.right), undefined];
47737
47747
  });
47738
47748
  const step4 = (queue) => exports_Effect.gen(function* () {
47739
- const currentQueue = queue ?? (yield* registerQueue(config2.http, config2.mode));
47749
+ const currentQueue = queue ?? (yield* registerQueue(config2.http, config2.mode, config2.queueIdleTimeoutSecs).pipe(exports_Effect.tap((q) => config2.onQueueRegister?.(q) ?? exports_Effect.void)));
47740
47750
  const res = yield* config2.http.get("/events", eventsResponseSchema, {
47741
47751
  queue_id: currentQueue.queueId,
47742
47752
  last_event_id: currentQueue.lastEventId
47743
47753
  });
47754
+ yield* reportResume(true);
47744
47755
  const directory = res.events.length > 0 ? yield* config2.resolveDirectory() : undefined;
47745
47756
  let maxId = currentQueue.lastEventId;
47746
47757
  const mapped = [];
@@ -47763,6 +47774,9 @@ var inboxEvents = (config2) => exports_Stream.unwrap(exports_Effect.gen(function
47763
47774
  yield* watermark.advance(ev.message.ts);
47764
47775
  }
47765
47776
  }
47777
+ if (config2.onQueueAdvance !== undefined && maxId > currentQueue.lastEventId) {
47778
+ yield* config2.onQueueAdvance(maxId);
47779
+ }
47766
47780
  return [mapped, { queueId: currentQueue.queueId, lastEventId: maxId }];
47767
47781
  }).pipe(exports_Effect.catchTag("ZulipApiError", (e) => {
47768
47782
  if (e.code === "BAD_EVENT_QUEUE_ID")
@@ -48406,7 +48420,7 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
48406
48420
  if (exports_Option.exists(state.registration, (r) => r.mode === mode)) {
48407
48421
  return exports_Effect.succeed([undefined, state]);
48408
48422
  }
48409
- return registerQueue(minterHttp, mode).pipe(exports_Effect.map((q) => [
48423
+ return registerQueue(minterHttp, mode, config2.queueIdleTimeoutSecs).pipe(exports_Effect.tap((q) => config2.onQueueRegister?.(q) ?? exports_Effect.void), exports_Effect.map((q) => [
48410
48424
  undefined,
48411
48425
  { ...state, registration: exports_Option.some(exports_Data.struct({ queue: q, mode })) }
48412
48426
  ]));
@@ -48463,23 +48477,35 @@ var zulipAdapter = (config2) => exports_Effect.gen(function* () {
48463
48477
  subscriptions: JSON.stringify([channel])
48464
48478
  }).pipe(exports_Effect.asVoid)));
48465
48479
  }).pipe(exports_Effect.mapError((cause3) => new InboxError({ operation: "unsubscribe", cause: cause3 }))),
48466
- events: () => exports_Stream.unwrap(exports_Effect.all([exports_SynchronizedRef.get(boundRef), exports_SynchronizedRef.get(inboxRef)]).pipe(exports_Effect.map(([current2, state]) => inboxEvents({
48467
- http: minterHttp,
48468
- permalinkBase: base,
48469
- resolveDirectory: buildDirectoryLookup,
48470
- mode: currentMode(state),
48471
- messageRefCache,
48472
- watermarkStore,
48473
- replay: (since) => inbox.replay(since),
48474
- ...exports_Option.match(current2, {
48475
- onNone: () => ({}),
48476
- onSome: (b) => ({ boundIdentity: b.identity })
48477
- }),
48478
- ...exports_Option.match(state.registration, {
48479
- onNone: () => ({}),
48480
- onSome: (r) => ({ initialQueue: r.queue })
48481
- })
48482
- })))).pipe(exports_Stream.filterEffect(shouldDeliver)),
48480
+ events: () => exports_Stream.unwrap(exports_Effect.all([
48481
+ exports_SynchronizedRef.get(boundRef),
48482
+ exports_SynchronizedRef.get(inboxRef),
48483
+ config2.resumeQueue?.() ?? exports_Effect.succeedNone
48484
+ ]).pipe(exports_Effect.flatMap(([current2, state, resumed]) => {
48485
+ const initialQueue = exports_Option.orElse(resumed, () => exports_Option.map(state.registration, (r) => r.queue));
48486
+ const reportAbsentResume = exports_Option.isNone(resumed) ? config2.onResumeOutcome?.(false) ?? exports_Effect.void : exports_Effect.void;
48487
+ return reportAbsentResume.pipe(exports_Effect.as(inboxEvents({
48488
+ http: minterHttp,
48489
+ permalinkBase: base,
48490
+ resolveDirectory: buildDirectoryLookup,
48491
+ mode: currentMode(state),
48492
+ messageRefCache,
48493
+ watermarkStore,
48494
+ ...config2.queueIdleTimeoutSecs === undefined ? {} : { queueIdleTimeoutSecs: config2.queueIdleTimeoutSecs },
48495
+ ...config2.onQueueRegister === undefined ? {} : { onQueueRegister: config2.onQueueRegister },
48496
+ ...config2.onQueueAdvance === undefined ? {} : { onQueueAdvance: config2.onQueueAdvance },
48497
+ replay: (since) => inbox.replay(since),
48498
+ ...exports_Option.match(current2, {
48499
+ onNone: () => ({}),
48500
+ onSome: (b) => ({ boundIdentity: b.identity })
48501
+ }),
48502
+ ...exports_Option.match(initialQueue, {
48503
+ onNone: () => ({}),
48504
+ onSome: (q) => ({ initialQueue: q })
48505
+ }),
48506
+ ...exports_Option.isSome(resumed) && config2.onResumeOutcome !== undefined ? { onResumeOutcome: config2.onResumeOutcome } : {}
48507
+ })));
48508
+ }))).pipe(exports_Stream.filterEffect(shouldDeliver)),
48483
48509
  replay: (since) => {
48484
48510
  const replayNarrow = JSON.stringify([{ negated: true, operator: "is", operand: "dm" }]);
48485
48511
  return exports_Effect.all([
@@ -56742,6 +56768,70 @@ class StdioServerTransport {
56742
56768
  }
56743
56769
  }
56744
56770
 
56771
+ // packages/mcp/queue-state-hooks.ts
56772
+ var buildQueueStateHooks = (deps) => {
56773
+ const { store, session, idleTimeoutSecs, resumeOutcome } = deps;
56774
+ const withSession = (use) => exports_Deferred.poll(session).pipe(exports_Effect.flatMap(exports_Option.match({
56775
+ onNone: () => exports_Effect.void,
56776
+ onSome: (awaitId) => exports_Effect.flatMap(awaitId, use)
56777
+ })));
56778
+ return {
56779
+ queueIdleTimeoutSecs: idleTimeoutSecs,
56780
+ onQueueRegister: (queue) => withSession((id3) => exports_Deferred.poll(resumeOutcome).pipe(exports_Effect.flatMap(exports_Option.match({
56781
+ onSome: () => store.write(id3, queue),
56782
+ onNone: () => store.read(id3).pipe(exports_Effect.map(exports_Option.isNone), exports_Effect.catchAll(() => exports_Effect.succeed(true)), exports_Effect.flatMap((isFresh3) => isFresh3 ? store.write(id3, queue) : exports_Effect.void))
56783
+ })), exports_Effect.catchAllCause(() => exports_Effect.void))),
56784
+ onQueueAdvance: (lastEventId) => withSession((id3) => store.advance(id3, lastEventId).pipe(exports_Effect.catchAllCause(() => exports_Effect.void))),
56785
+ resumeQueue: () => exports_Deferred.poll(session).pipe(exports_Effect.flatMap(exports_Option.match({
56786
+ onNone: () => exports_Effect.succeedNone,
56787
+ onSome: (awaitId) => exports_Effect.flatMap(awaitId, (id3) => store.read(id3).pipe(exports_Effect.catchAllCause(() => exports_Effect.succeedNone)))
56788
+ }))),
56789
+ onResumeOutcome: (queueReplayed) => exports_Deferred.succeed(resumeOutcome, queueReplayed).pipe(exports_Effect.asVoid)
56790
+ };
56791
+ };
56792
+
56793
+ // packages/mcp/queue-state-store.ts
56794
+ import { homedir } from "node:os";
56795
+ import { join as join7 } from "node:path";
56796
+ class QueueStateStoreTag extends exports_Context.Tag("QueueStateStore")() {
56797
+ }
56798
+ var FILENAME_SAFE = /[^a-zA-Z0-9._-]/g;
56799
+ var stateFilename = (id3) => `${id3.replace(FILENAME_SAFE, "_")}.json`;
56800
+ var QueueStateFileSchema = exports_Schema.parseJson(exports_Schema.Struct({ queueId: exports_Schema.String, lastEventId: exports_Schema.Int }));
56801
+ var decodeQueueStateFile = exports_Schema.decodeUnknown(QueueStateFileSchema);
56802
+ var encodeQueueStateFile = exports_Schema.encode(QueueStateFileSchema);
56803
+ var isNotFound = (error2) => error2._tag === "SystemError" && error2.reason === "NotFound";
56804
+ var readState = (fs, path) => fs.readFileString(path).pipe(exports_Effect.flatMap(decodeQueueStateFile), exports_Effect.map(exports_Option.some), exports_Effect.catchIf(isNotFound, () => exports_Effect.succeed(exports_Option.none())));
56805
+ var createFileQueueStateStore = (deps) => {
56806
+ const { dir, fs } = deps;
56807
+ const pathFor = (id3) => join7(dir, stateFilename(id3));
56808
+ const persist = (path, state) => encodeQueueStateFile(state).pipe(exports_Effect.orDie, exports_Effect.flatMap((json4) => fs.makeDirectory(dir, { recursive: true }).pipe(exports_Effect.zipRight(fs.writeFileString(path, json4)))));
56809
+ const read3 = (id3) => readState(fs, pathFor(id3));
56810
+ const write4 = (id3, state) => persist(pathFor(id3), state);
56811
+ const advance = (id3, lastEventId) => {
56812
+ const path = pathFor(id3);
56813
+ return readState(fs, path).pipe(exports_Effect.catchTag("ParseError", () => exports_Effect.succeed(exports_Option.none())), exports_Effect.flatMap(exports_Option.match({
56814
+ onNone: () => exports_Effect.void,
56815
+ onSome: (prior) => lastEventId > prior.lastEventId ? persist(path, { queueId: prior.queueId, lastEventId }) : exports_Effect.void
56816
+ })));
56817
+ };
56818
+ return { read: read3, write: write4, advance };
56819
+ };
56820
+ var STATE_SEGMENT = "commy";
56821
+ var stateBaseConfig = exports_Config.nonEmptyString("XDG_STATE_HOME").pipe(exports_Config.withDefault(join7(homedir(), ".local", "state")));
56822
+ var queueStateDirConfig = stateBaseConfig.pipe(exports_Config.map((base) => join7(base, STATE_SEGMENT, "queue-state")));
56823
+ var FileQueueStateStoreLive = exports_Layer.effect(QueueStateStoreTag, exports_Effect.all([exports_FileSystem.FileSystem, exports_Effect.orDie(queueStateDirConfig)]).pipe(exports_Effect.map(([fs, dir]) => createFileQueueStateStore({ dir, fs }))));
56824
+
56825
+ // packages/mcp/resume-outcome.ts
56826
+ class ResumeOutcome extends exports_Context.Tag("commy/mcp/ResumeOutcome")() {
56827
+ }
56828
+ var ResumeOutcomeLive = exports_Layer.effect(ResumeOutcome, exports_Deferred.make());
56829
+
56830
+ // packages/mcp/session-id.ts
56831
+ class SessionId extends exports_Context.Tag("commy/mcp/SessionId")() {
56832
+ }
56833
+ var SessionIdLive = exports_Layer.effect(SessionId, exports_Deferred.make());
56834
+
56745
56835
  // packages/mcp/subscribe-parser.ts
56746
56836
  class SubscribeTokenError extends exports_Data.TaggedError("SubscribeTokenError") {
56747
56837
  get message() {
@@ -56867,6 +56957,14 @@ var userConfigString = (key) => exports_Config.string(key).pipe(exports_Config.m
56867
56957
  return exports_Either.right(value5);
56868
56958
  }));
56869
56959
  var optionalUserConfig = (key) => exports_Config.option(userConfigString(key));
56960
+ var QUEUE_IDLE_TIMEOUT_DEFAULT_SECS = 86400;
56961
+ var queueIdleTimeoutSecs = (key) => optionalUserConfig(key).pipe(exports_Config.mapOrFail((option7) => exports_Option.match(option7, {
56962
+ onNone: () => exports_Either.right(QUEUE_IDLE_TIMEOUT_DEFAULT_SECS),
56963
+ onSome: (raw3) => {
56964
+ const parsed = Number(raw3);
56965
+ return Number.isInteger(parsed) && parsed > 0 ? exports_Either.right(Math.min(parsed, MAX_QUEUE_TIMEOUT_SECS)) : exports_Either.left(exports_ConfigError.InvalidData([key], `${key} must be a positive integer — received: ${raw3}`));
56966
+ }
56967
+ })));
56870
56968
  var optionalNonNegativeInt = (key) => optionalUserConfig(key).pipe(exports_Config.mapOrFail((option7) => exports_Option.match(option7, {
56871
56969
  onNone: () => exports_Either.right(exports_Option.none()),
56872
56970
  onSome: (raw3) => {
@@ -56896,7 +56994,9 @@ var envConfig = exports_Config.all({
56896
56994
  botApiKey: optionalApiKey("COMMY_BOT_API_KEY"),
56897
56995
  subscribe: optionalUserConfig("COMMY_SUBSCRIBE"),
56898
56996
  project: optionalNonEmpty("COMMY_PROJECT"),
56899
- catchupWindowSeconds: optionalNonNegativeInt("COMMY_CATCHUP_WINDOW_SECONDS")
56997
+ catchupWindowSeconds: optionalNonNegativeInt("COMMY_CATCHUP_WINDOW_SECONDS"),
56998
+ queueIdleTimeoutSecs: queueIdleTimeoutSecs("COMMY_QUEUE_IDLE_TIMEOUT_SECS"),
56999
+ downloadDir: optionalNonEmpty("COMMY_DOWNLOAD_DIR")
56900
57000
  }).pipe(exports_Config.mapOrFail((raw3) => {
56901
57001
  if (exports_Option.isSome(raw3.botApiKey) && exports_Option.isNone(raw3.botName)) {
56902
57002
  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"));
@@ -56906,6 +57006,7 @@ var envConfig = exports_Config.all({
56906
57006
  realmUrl: raw3.realmUrl,
56907
57007
  minterEmail: raw3.minterEmail,
56908
57008
  minterApiKey: raw3.minterApiKey,
57009
+ queueIdleTimeoutSecs: raw3.queueIdleTimeoutSecs,
56909
57010
  ...exports_Option.match(raw3.botName, { onNone: () => ({}), onSome: (botName) => ({ botName }) }),
56910
57011
  ...exports_Option.isSome(raw3.botName) && exports_Option.isSome(raw3.botApiKey) ? { attachIdentity: { name: raw3.botName.value, apiKey: raw3.botApiKey.value } } : {},
56911
57012
  ...exports_Option.match(raw3.subscribe, {
@@ -56916,6 +57017,10 @@ var envConfig = exports_Config.all({
56916
57017
  ...exports_Option.match(raw3.catchupWindowSeconds, {
56917
57018
  onNone: () => ({}),
56918
57019
  onSome: (catchupWindowSeconds) => ({ catchupWindowSeconds })
57020
+ }),
57021
+ ...exports_Option.match(raw3.downloadDir, {
57022
+ onNone: () => ({}),
57023
+ onSome: (downloadDir) => ({ downloadDir })
56919
57024
  })
56920
57025
  });
56921
57026
  }));
@@ -56975,11 +57080,24 @@ class SubstrateAdapter extends exports_Context.Tag("SubstrateAdapter")() {
56975
57080
  var substrateAdapterLayer = (make78) => exports_Layer.scoped(SubstrateAdapter, exports_Effect.tap(make78, (adapter4) => exports_Effect.addFinalizer(() => exports_Effect.promise(() => adapter4.close()))));
56976
57081
  var ZulipAdapterLive = substrateAdapterLayer(exports_Effect.gen(function* () {
56977
57082
  const parsed = yield* parseEnv;
57083
+ const queueHooks = parsed.botName === undefined ? buildQueueStateHooks({
57084
+ store: yield* QueueStateStoreTag,
57085
+ session: yield* SessionId,
57086
+ idleTimeoutSecs: parsed.queueIdleTimeoutSecs,
57087
+ resumeOutcome: yield* ResumeOutcome
57088
+ }) : undefined;
56978
57089
  return yield* zulipAdapter({
56979
57090
  realmUrl: parsed.realmUrl,
56980
57091
  minterEmail: parsed.minterEmail,
56981
57092
  minterApiKey: parsed.minterApiKey,
56982
- ...parsed.attachIdentity === undefined ? {} : { attachIdentity: parsed.attachIdentity }
57093
+ ...parsed.attachIdentity === undefined ? {} : { attachIdentity: parsed.attachIdentity },
57094
+ ...queueHooks === undefined ? {} : {
57095
+ queueIdleTimeoutSecs: queueHooks.queueIdleTimeoutSecs,
57096
+ onQueueRegister: queueHooks.onQueueRegister,
57097
+ onQueueAdvance: queueHooks.onQueueAdvance,
57098
+ resumeQueue: queueHooks.resumeQueue,
57099
+ onResumeOutcome: queueHooks.onResumeOutcome
57100
+ }
56983
57101
  });
56984
57102
  }));
56985
57103
  var subscribeFromEnv = (inbox, narrowSet, parsed) => {
@@ -57106,29 +57224,29 @@ var catchUpChannels = (deps) => exports_Effect.gen(function* () {
57106
57224
  });
57107
57225
 
57108
57226
  // packages/mcp/cursor-store.ts
57109
- import { homedir } from "node:os";
57110
- import { join as join7 } from "node:path";
57227
+ import { homedir as homedir2 } from "node:os";
57228
+ import { join as join8 } from "node:path";
57111
57229
  class CursorStoreTag extends exports_Context.Tag("CursorStore")() {
57112
57230
  }
57113
- var FILENAME_SAFE = /[^a-zA-Z0-9._-]/g;
57231
+ var FILENAME_SAFE2 = /[^a-zA-Z0-9._-]/g;
57114
57232
  var cursorFilename = (id3) => {
57115
- const safe = id3.replace(FILENAME_SAFE, "_");
57233
+ const safe = id3.replace(FILENAME_SAFE2, "_");
57116
57234
  return `${safe}.json`;
57117
57235
  };
57118
57236
  var CursorFileSchema = exports_Schema.parseJson(exports_Schema.Struct({ lastSeenTs: TimestampSchema }));
57119
57237
  var decodeCursorFile = exports_Schema.decodeUnknown(CursorFileSchema);
57120
- var isNotFound = (error2) => error2._tag === "SystemError" && error2.reason === "NotFound";
57121
- var readCursor = (fs, path) => fs.readFileString(path).pipe(exports_Effect.flatMap(decodeCursorFile), exports_Effect.map((file3) => exports_Option.some(file3.lastSeenTs)), exports_Effect.catchIf(isNotFound, () => exports_Effect.succeed(exports_Option.none())));
57238
+ var isNotFound2 = (error2) => error2._tag === "SystemError" && error2.reason === "NotFound";
57239
+ var readCursor = (fs, path) => fs.readFileString(path).pipe(exports_Effect.flatMap(decodeCursorFile), exports_Effect.map((file3) => exports_Option.some(file3.lastSeenTs)), exports_Effect.catchIf(isNotFound2, () => exports_Effect.succeed(exports_Option.none())));
57122
57240
  var createFileCursorStore = (deps) => {
57123
57241
  const { dir, fs } = deps;
57124
- const pathFor = (id3) => join7(dir, cursorFilename(id3));
57242
+ const pathFor = (id3) => join8(dir, cursorFilename(id3));
57125
57243
  const read3 = (id3) => readCursor(fs, pathFor(id3));
57126
57244
  const write4 = (id3, ts) => readCursor(fs, pathFor(id3)).pipe(exports_Effect.catchTag("ParseError", () => exports_Effect.succeed(exports_Option.none())), exports_Effect.flatMap((prior) => exports_Option.isSome(prior) && prior.value >= ts ? exports_Effect.void : fs.makeDirectory(dir, { recursive: true }).pipe(exports_Effect.zipRight(fs.writeFileString(pathFor(id3), JSON.stringify({ lastSeenTs: ts }))))));
57127
57245
  return { read: read3, write: write4 };
57128
57246
  };
57129
- var STATE_SEGMENT = "commy";
57130
- var stateBaseConfig = exports_Config.nonEmptyString("XDG_STATE_HOME").pipe(exports_Config.withDefault(join7(homedir(), ".local", "state")));
57131
- var cursorDirConfig = stateBaseConfig.pipe(exports_Config.map((base) => join7(base, STATE_SEGMENT, "cursors")));
57247
+ var STATE_SEGMENT2 = "commy";
57248
+ var stateBaseConfig2 = exports_Config.nonEmptyString("XDG_STATE_HOME").pipe(exports_Config.withDefault(join8(homedir2(), ".local", "state")));
57249
+ var cursorDirConfig = stateBaseConfig2.pipe(exports_Config.map((base) => join8(base, STATE_SEGMENT2, "cursors")));
57132
57250
  var FileCursorStoreLive = exports_Layer.effect(CursorStoreTag, exports_Effect.all([exports_FileSystem.FileSystem, exports_Effect.orDie(cursorDirConfig)]).pipe(exports_Effect.map(([fs, dir]) => createFileCursorStore({ dir, fs }))));
57133
57251
 
57134
57252
  // packages/mcp/ensure-bound.ts
@@ -57164,35 +57282,6 @@ var createEnsureBound = (deps) => exports_Effect.map(exports_Ref.make({ kind: "i
57164
57282
  return ensureBound;
57165
57283
  });
57166
57284
 
57167
- // packages/mcp/target-cursor.ts
57168
- import { homedir as homedir2 } from "node:os";
57169
- import { join as join8 } from "node:path";
57170
- var deliveryTargetOf = (message) => exports_Data.struct({
57171
- channel: message.ref.channel.name,
57172
- thread: exports_Option.map(message.ref.thread, (observed) => observed.name)
57173
- });
57174
- var DeliveryTargetSchema = exports_Schema.Data(exports_Schema.Struct({
57175
- channel: ChannelNameSchema,
57176
- thread: exports_Schema.OptionFromNullOr(ThreadNameSchema)
57177
- }));
57178
- var TargetCursorsFileSchema = exports_Schema.parseJson(exports_Schema.HashMap({ key: DeliveryTargetSchema, value: TimestampSchema }));
57179
- var decodeTargetCursorsFile = exports_Schema.decodeUnknown(TargetCursorsFileSchema);
57180
- var encodeTargetCursorsFile = exports_Schema.encode(TargetCursorsFileSchema);
57181
- var FILENAME_SAFE2 = /[^a-zA-Z0-9._-]/g;
57182
- var cursorsFilename = (id3) => `${id3.replace(FILENAME_SAFE2, "_")}.json`;
57183
- var isNotFound2 = (error2) => error2._tag === "SystemError" && error2.reason === "NotFound";
57184
- var readCursors = (fs, path) => fs.readFileString(path).pipe(exports_Effect.flatMap(decodeTargetCursorsFile), exports_Effect.map(exports_Option.some), exports_Effect.catchIf(isNotFound2, () => exports_Effect.succeed(exports_Option.none())));
57185
- var advanceTargetCursor = (fs, dir, id3, target, ts) => {
57186
- const path = join8(dir, cursorsFilename(id3));
57187
- return readCursors(fs, path).pipe(exports_Effect.catchTag("ParseError", () => exports_Effect.succeed(exports_Option.none())), exports_Effect.map(exports_Option.getOrElse(() => exports_HashMap.empty())), exports_Effect.flatMap((cursors) => {
57188
- const prior = exports_HashMap.get(cursors, target);
57189
- return exports_Option.isSome(prior) && prior.value >= ts ? exports_Effect.void : encodeTargetCursorsFile(exports_HashMap.set(cursors, target, ts)).pipe(exports_Effect.orDie, exports_Effect.flatMap((json4) => fs.makeDirectory(dir, { recursive: true }).pipe(exports_Effect.zipRight(fs.writeFileString(path, json4)))));
57190
- }));
57191
- };
57192
- var STATE_SEGMENT2 = "commy";
57193
- var stateBaseConfig2 = exports_Config.nonEmptyString("XDG_STATE_HOME").pipe(exports_Config.withDefault(join8(homedir2(), ".local", "state")));
57194
- var targetCursorDirConfig = stateBaseConfig2.pipe(exports_Config.map((base) => join8(base, STATE_SEGMENT2, "target-cursors")));
57195
-
57196
57285
  // packages/mcp/event-pump.ts
57197
57286
  var originatorId = (event) => exports_Match.value(event).pipe(exports_Match.discriminatorsExhaustive("kind")({
57198
57287
  "message-posted": (message) => message.message.sender.id,
@@ -57247,9 +57336,7 @@ var startEventPump = (deps) => exports_Effect.gen(function* () {
57247
57336
  for (const identity6 of identitiesIn(event))
57248
57337
  deps.rememberIdentity(identity6);
57249
57338
  }
57250
- const mentionAdvance = deps.onMention !== undefined && event.kind === "mention-received" ? deps.onMention(event.message.ts) : exports_Effect.void;
57251
- const deliveryAdvance = deps.onDelivery !== undefined && (event.kind === "message-posted" || event.kind === "mention-received") ? deps.onDelivery(deliveryTargetOf(event.message), event.message.ts) : exports_Effect.void;
57252
- const advance = exports_Effect.zipRight(mentionAdvance, deliveryAdvance);
57339
+ const advance = deps.onMention !== undefined && event.kind === "mention-received" ? deps.onMention(event.message.ts) : exports_Effect.void;
57253
57340
  if (recordMessageDelivery(event, seenDelivered) === "duplicate")
57254
57341
  return advance;
57255
57342
  return advance.pipe(exports_Effect.zipRight(renderEvent(event, botIdentityId)), exports_Effect.flatMap((payload) => exports_Effect.tryPromise({
@@ -58762,7 +58849,7 @@ class Server extends Protocol {
58762
58849
  }
58763
58850
 
58764
58851
  // packages/mcp/mcp-server.ts
58765
- var PLUGIN_VERSION = "0.17.0";
58852
+ var PLUGIN_VERSION = "0.19.0";
58766
58853
  var COMMY_INSTRUCTIONS = `**Substrate.** commy is the inter-agent channel: agents and humans coordinate here. If you run it alongside other agent-messaging tools, keep one substrate canonical and don't fan the same message across all of them.
58767
58854
 
58768
58855
  **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.
@@ -58773,7 +58860,7 @@ var COMMY_INSTRUCTIONS = `**Substrate.** commy is the inter-agent channel: agent
58773
58860
 
58774
58861
  **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}\`.
58775
58862
 
58776
- **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).
58863
+ **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 — rooted under the operator-set \`COMMY_DOWNLOAD_DIR\` when configured so it lands somewhere you can Read, else \`$TMPDIR\`; 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).
58777
58864
 
58778
58865
  **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. 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.`;
58779
58866
  var buildMcpServer = () => new Server({ name: "commy", version: PLUGIN_VERSION }, {
@@ -58879,11 +58966,6 @@ var createNarrowSet = () => {
58879
58966
  var DEFAULT_TIMEOUT_MS = 5000;
58880
58967
  var raceReleaseAgainstTimeout = (release, timeoutMs = DEFAULT_TIMEOUT_MS) => release.pipe(exports_Effect.tapDefect((cause3) => exports_Effect.logError(`release failed: ${exports_Cause.pretty(cause3)}`)), exports_Effect.catchAllDefect(() => exports_Effect.void), exports_Effect.timeoutOption(exports_Duration.millis(timeoutMs)), exports_Effect.flatMap((result) => exports_Option.isNone(result) ? exports_Effect.logError(`release timed out after ${timeoutMs}ms`) : exports_Effect.void));
58881
58968
 
58882
- // packages/mcp/session-id.ts
58883
- class SessionId extends exports_Context.Tag("commy/mcp/SessionId")() {
58884
- }
58885
- var SessionIdLive = exports_Layer.effect(SessionId, exports_Deferred.make());
58886
-
58887
58969
  // packages/mcp/subscription-restore.ts
58888
58970
  var applyRestored = (deps, intents) => exports_Effect.sync(() => deps.narrowSet.load(exports_Option.some(intents))).pipe(exports_Effect.zipRight(exports_Effect.forEach(intents, (intent) => deps.inbox.subscribe(intentToTarget(intent)), {
58889
58971
  discard: true
@@ -58918,25 +59000,16 @@ var subscriptionFilename = (id3) => {
58918
59000
  var isNotFound3 = (error2) => error2._tag === "SystemError" && error2.reason === "NotFound";
58919
59001
  var readSubscriptions = (fs, path) => fs.readFileString(path).pipe(exports_Effect.flatMap(decodeSubscriptionsFile), exports_Effect.map((intents) => exports_Option.some(intents)), exports_Effect.catchIf(isNotFound3, () => exports_Effect.succeed(exports_Option.none())));
58920
59002
  var createFileSubscriptionStore = (deps) => {
58921
- const { dir, fs, cursorDir, session } = deps;
59003
+ const { dir, fs, session } = deps;
58922
59004
  const pathFor = (id3) => join9(dir, subscriptionFilename(id3));
58923
59005
  const read3 = () => exports_Effect.flatMap(exports_Deferred.await(session), (id3) => readSubscriptions(fs, pathFor(id3)));
58924
59006
  const write4 = (intents) => exports_Effect.flatMap(exports_Deferred.await(session), (id3) => fs.makeDirectory(dir, { recursive: true }).pipe(exports_Effect.zipRight(fs.writeFileString(pathFor(id3), JSON.stringify(intents)))));
58925
- const advanceCursor = (target, ts) => exports_Effect.flatMap(exports_Deferred.poll(session), exports_Option.match({
58926
- onNone: () => exports_Effect.void,
58927
- onSome: (awaitId) => exports_Effect.flatMap(awaitId, (id3) => advanceTargetCursor(fs, cursorDir, id3, target, ts))
58928
- }));
58929
- return { read: read3, write: write4, advanceCursor };
59007
+ return { read: read3, write: write4 };
58930
59008
  };
58931
59009
  var STATE_SEGMENT3 = "commy";
58932
59010
  var stateBaseConfig3 = exports_Config.nonEmptyString("XDG_STATE_HOME").pipe(exports_Config.withDefault(join9(homedir3(), ".local", "state")));
58933
59011
  var subscriptionDirConfig = stateBaseConfig3.pipe(exports_Config.map((base) => join9(base, STATE_SEGMENT3, "subscriptions")));
58934
- var FileSubscriptionStoreLive = exports_Layer.effect(SubscriptionStoreTag, exports_Effect.all([
58935
- exports_FileSystem.FileSystem,
58936
- exports_Effect.orDie(subscriptionDirConfig),
58937
- exports_Effect.orDie(targetCursorDirConfig),
58938
- SessionId
58939
- ]).pipe(exports_Effect.map(([fs, dir, cursorDir, session]) => createFileSubscriptionStore({ dir, fs, cursorDir, session }))));
59012
+ var FileSubscriptionStoreLive = exports_Layer.effect(SubscriptionStoreTag, exports_Effect.all([exports_FileSystem.FileSystem, exports_Effect.orDie(subscriptionDirConfig), SessionId]).pipe(exports_Effect.map(([fs, dir, session]) => createFileSubscriptionStore({ dir, fs, session }))));
58940
59013
 
58941
59014
  // packages/mcp/tools.ts
58942
59015
  var emptyObjectSchema = {
@@ -59602,7 +59675,7 @@ var buildToolDefs = (deps, cache) => {
59602
59675
  const download = deps.downloadFile;
59603
59676
  optionalTools.push({
59604
59677
  name: "download_file",
59605
- description: "Download a Zulip user upload by its /user_uploads/... path (visible in message bodies). Writes the file to a temp path and returns {file_path, content_type, size}. Use the Read tool on the returned file_path to view images.",
59678
+ description: "Download a Zulip user upload by its /user_uploads/... path (visible in message bodies). Writes the file into a fresh temp directory and returns {file_path, content_type, size}. The temp directory is created under the operator-set COMMY_DOWNLOAD_DIR when configured (so the file lands in a directory you can Read), otherwise under $TMPDIR. Use the Read tool on the returned file_path to view images.",
59606
59679
  inputSchema: {
59607
59680
  type: "object",
59608
59681
  properties: {
@@ -59739,12 +59812,22 @@ var makeProgram = (params = {}) => exports_Effect.scoped(exports_Effect.gen(func
59739
59812
  const cursorStore = yield* CursorStoreTag;
59740
59813
  const subscriptionStore = yield* SubscriptionStoreTag;
59741
59814
  const sessionIdDeferred = yield* SessionId;
59815
+ const resumeOutcome = yield* ResumeOutcome;
59742
59816
  yield* exports_Effect.flatMap(readBootSessionId, exports_Option.match({
59743
59817
  onNone: () => exports_Effect.void,
59744
59818
  onSome: (sessionId) => exports_Deferred.succeed(sessionIdDeferred, sessionId).pipe(exports_Effect.asVoid)
59745
59819
  }));
59746
59820
  const feedSessionId = (sessionId) => exports_Deferred.succeed(sessionIdDeferred, sessionId).pipe(exports_Effect.asVoid);
59747
59821
  const fs = yield* exports_FileSystem.FileSystem;
59822
+ if (parsed.downloadDir !== undefined) {
59823
+ const base = parsed.downloadDir;
59824
+ const baseStat = yield* fs.stat(base).pipe(exports_Effect.option);
59825
+ if (exports_Option.isNone(baseStat) || baseStat.value.type !== "Directory") {
59826
+ return yield* envConfigError([
59827
+ `COMMY_DOWNLOAD_DIR is set to ${base} but is not an existing directory`
59828
+ ]);
59829
+ }
59830
+ }
59748
59831
  const loggerLayer = params.loggerLayer ?? stderrLoggerLayer;
59749
59832
  const commandExecutor = yield* exports_CommandExecutor.CommandExecutor;
59750
59833
  const readGitContext2 = params.readGitContext ?? ((cwd) => readGitContext(cwd).pipe(exports_Effect.provideService(exports_CommandExecutor.CommandExecutor, commandExecutor)));
@@ -59777,12 +59860,25 @@ var makeProgram = (params = {}) => exports_Effect.scoped(exports_Effect.gen(func
59777
59860
  onNone: () => exports_Effect.void,
59778
59861
  onSome: () => persistSubscriptions(subscriptionStore, narrowSet).pipe(exports_Effect.catchAll((err) => exports_Effect.logError(`commy plugin: subscription persist failed: ${exports_Predicate.isError(err) ? err.message : String(err)}`)), exports_Effect.provide(loggerLayer))
59779
59862
  })));
59780
- const ephemeralOnAcquire = parsed.botName === undefined ? (acquired, project3, sessionId) => ensureSessionSubscriptions(sessionId, project3).pipe(exports_Effect.zipRight(catchUpMentions({
59781
- cursorStore,
59782
- inbox: adapter4.inbox,
59783
- identityId: acquired.identity.id,
59784
- notifier
59785
- }).pipe(exports_Effect.catchAllCause(logCatchUpFailure("ephemeral mentions")))), exports_Effect.provide(loggerLayer)) : undefined;
59863
+ const ephemeralOnAcquire = parsed.botName === undefined ? (acquired, project3, sessionId) => ensureSessionSubscriptions(sessionId, project3).pipe(exports_Effect.zipRight(exports_Deferred.await(resumeOutcome)), exports_Effect.flatMap((queueReplayed) => {
59864
+ if (queueReplayed)
59865
+ return exports_Effect.void;
59866
+ const identityId = acquired.identity.id;
59867
+ const windowSeconds = parsed.catchupWindowSeconds ?? DEFAULT_CATCHUP_WINDOW_SECONDS;
59868
+ const catchUpIntents = narrowSet.intents();
59869
+ return catchUpMentions({
59870
+ cursorStore,
59871
+ inbox: adapter4.inbox,
59872
+ identityId,
59873
+ notifier
59874
+ }).pipe(exports_Effect.catchAllCause(logCatchUpFailure("ephemeral mentions")), exports_Effect.zipRight(windowSeconds > 0 && catchUpIntents.length > 0 ? catchUpChannels({
59875
+ intents: catchUpIntents,
59876
+ history: adapter4.history,
59877
+ notifier,
59878
+ botIdentityId: identityId,
59879
+ windowSeconds
59880
+ }).pipe(exports_Effect.catchAllCause(logCatchUpFailure("ephemeral channels"))) : exports_Effect.void));
59881
+ }), exports_Effect.provide(loggerLayer)) : undefined;
59786
59882
  const identityCache = yield* buildIdentityCache(adapter4, parsed.botName, ephemeralOnAcquire);
59787
59883
  let type1Intents = [];
59788
59884
  if (parsed.botName !== undefined) {
@@ -59813,7 +59909,7 @@ var makeProgram = (params = {}) => exports_Effect.scoped(exports_Effect.gen(func
59813
59909
  feedSessionId,
59814
59910
  downloadFile: (urlPath) => exports_Effect.gen(function* () {
59815
59911
  const result = yield* adapter4.downloadFile(urlPath);
59816
- const dir = yield* fs.makeTempDirectory({ prefix: "commy-dl-" });
59912
+ const dir = yield* fs.makeTempDirectory(parsed.downloadDir === undefined ? { prefix: "commy-dl-" } : { directory: parsed.downloadDir, prefix: "commy-dl-" });
59817
59913
  const filePath = join10(dir, basename2(urlPath));
59818
59914
  yield* fs.writeFile(filePath, result.data);
59819
59915
  return { filePath, contentType: result.contentType, size: result.data.byteLength };
@@ -59867,11 +59963,6 @@ var makeProgram = (params = {}) => exports_Effect.scoped(exports_Effect.gen(func
59867
59963
  if (id3 === undefined)
59868
59964
  return exports_Effect.void;
59869
59965
  return cursorStore.write(id3, ts).pipe(exports_Effect.catchAllCause(() => exports_Effect.void));
59870
- },
59871
- onDelivery: (target, ts) => {
59872
- if (parsed.botName !== undefined)
59873
- return exports_Effect.void;
59874
- return subscriptionStore.advanceCursor(target, ts).pipe(exports_Effect.catchAllCause(() => exports_Effect.void));
59875
59966
  }
59876
59967
  });
59877
59968
  yield* exports_Effect.addFinalizer(() => pump.cancel);
@@ -59881,7 +59972,7 @@ var makeProgram = (params = {}) => exports_Effect.scoped(exports_Effect.gen(func
59881
59972
  yield* exports_Effect.forkScoped(restoreOnResume);
59882
59973
  yield* exports_Effect.raceFirst(pump.done, params.shutdownSignal ?? exports_Effect.never);
59883
59974
  }));
59884
- var AppLayer = exports_Layer.mergeAll(ZulipAdapterLive, FileCursorStoreLive, exports_Layer.provideMerge(FileSubscriptionStoreLive, SessionIdLive), stderrLoggerLayer);
59975
+ var AppLayer = exports_Layer.mergeAll(ZulipAdapterLive, FileCursorStoreLive, FileSubscriptionStoreLive, stderrLoggerLayer).pipe(exports_Layer.provideMerge(FileQueueStateStoreLive), exports_Layer.provideMerge(SessionIdLive), exports_Layer.provideMerge(ResumeOutcomeLive));
59885
59976
  var PlatformLive = exports_Layer.mergeAll(exports_Layer.setConfigProvider(exports_ConfigProvider.fromEnv()), exports_FetchHttpClient.layer, exports_NodeContext.layer);
59886
59977
  var MainLive = exports_Layer.provideMerge(AppLayer, PlatformLive);
59887
59978
  var clientDisconnect = (stdin3) => exports_Effect.async((resume2) => {