@hasna/conversations 0.2.29 → 0.2.30

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.
package/bin/index.js CHANGED
@@ -14928,7 +14928,7 @@ var init_presence = __esm(() => {
14928
14928
  var require_package = __commonJS((exports, module) => {
14929
14929
  module.exports = {
14930
14930
  name: "@hasna/conversations",
14931
- version: "0.2.29",
14931
+ version: "0.2.30",
14932
14932
  description: "Real-time CLI messaging for AI agents",
14933
14933
  type: "module",
14934
14934
  bin: {
@@ -15709,6 +15709,125 @@ var init_graph = __esm(() => {
15709
15709
  init_db();
15710
15710
  });
15711
15711
 
15712
+ // src/cli/commands/tmux.ts
15713
+ import chalk9 from "chalk";
15714
+ import { execSync } from "child_process";
15715
+ function sleep(ms) {
15716
+ return new Promise((resolve) => setTimeout(resolve, ms));
15717
+ }
15718
+ async function tmuxSend(target, message, opts = {}) {
15719
+ const delay = opts.delayMs ?? Math.max(12000, message.length * 50);
15720
+ const maxRetries = opts.retries ?? 3;
15721
+ const verify = opts.verify !== false;
15722
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
15723
+ execSync(`tmux send-keys -t ${JSON.stringify(target)} -l ${JSON.stringify(message)}`);
15724
+ await sleep(delay);
15725
+ execSync(`tmux send-keys -t ${JSON.stringify(target)} Enter`);
15726
+ if (!verify)
15727
+ return { success: true, attempts: attempt };
15728
+ await sleep(500);
15729
+ const pane = execSync(`tmux capture-pane -t ${JSON.stringify(target)} -p`).toString();
15730
+ const lastLines = pane.split(`
15731
+ `).slice(-3).join(`
15732
+ `);
15733
+ if (!lastLines.includes(message.slice(0, 20))) {
15734
+ return { success: true, attempts: attempt };
15735
+ }
15736
+ if (attempt < maxRetries)
15737
+ await sleep(2000);
15738
+ }
15739
+ return { success: false, attempts: maxRetries };
15740
+ }
15741
+ function registerTmuxCommands(program2) {
15742
+ const tmux = program2.command("tmux").description("Dispatch messages to tmux windows (Claude Code sessions)");
15743
+ tmux.command("send").description("Send a message to a tmux window with paste+wait+Enter+verify").requiredOption("--target <target>", "Tmux target: session:window or session:window.pane").requiredOption("--message <text>", "Message text to send").option("--delay <ms>", "Wait time (ms) after paste before hitting Enter (default: max(12000, len*50))", parseInt).option("--retries <n>", "Max retry attempts (default: 3)", parseInt).option("--no-verify", "Skip verification after sending").option("--json", "Output result as JSON").action(async (opts) => {
15744
+ const target = opts.target.trim();
15745
+ const message = opts.message;
15746
+ if (!target) {
15747
+ console.error(chalk9.red("--target is required."));
15748
+ process.exit(1);
15749
+ }
15750
+ if (!message || !message.trim()) {
15751
+ console.error(chalk9.red("--message cannot be empty."));
15752
+ process.exit(1);
15753
+ }
15754
+ try {
15755
+ const result = await tmuxSend(target, message, {
15756
+ delayMs: Number.isFinite(opts.delay) ? opts.delay : undefined,
15757
+ retries: Number.isFinite(opts.retries) ? opts.retries : undefined,
15758
+ verify: opts.verify !== false
15759
+ });
15760
+ if (opts.json) {
15761
+ console.log(JSON.stringify({ target, result }));
15762
+ } else if (result.success) {
15763
+ console.log(chalk9.green(`Sent to ${target}`) + chalk9.dim(` (attempt ${result.attempts})`));
15764
+ } else {
15765
+ console.error(chalk9.red(`Failed to confirm delivery to ${target}`) + chalk9.dim(` after ${result.attempts} attempt(s)`));
15766
+ process.exit(1);
15767
+ }
15768
+ } catch (err) {
15769
+ const msg = err instanceof Error ? err.message : String(err);
15770
+ if (opts.json) {
15771
+ console.log(JSON.stringify({ target, error: msg }));
15772
+ } else {
15773
+ console.error(chalk9.red(`tmux error: ${msg}`));
15774
+ }
15775
+ process.exit(1);
15776
+ }
15777
+ });
15778
+ tmux.command("broadcast").description("Send the same message to multiple tmux windows").requiredOption("--targets <list>", "Comma-separated list of tmux targets").requiredOption("--message <text>", "Message text to send").option("--delay <ms>", "Wait time (ms) after paste before Enter (default: max(12000, len*50))", parseInt).option("--stagger <ms>", "Delay (ms) between each target (default: 0)", parseInt).option("--retries <n>", "Max retry attempts per target (default: 3)", parseInt).option("--no-verify", "Skip verification after sending").option("--json", "Output results as JSON").action(async (opts) => {
15779
+ const targets = opts.targets.split(",").map((t) => t.trim()).filter(Boolean);
15780
+ const message = opts.message;
15781
+ const stagger = Number.isFinite(opts.stagger) && opts.stagger > 0 ? opts.stagger : 0;
15782
+ if (targets.length === 0) {
15783
+ console.error(chalk9.red("--targets must be a non-empty comma-separated list."));
15784
+ process.exit(1);
15785
+ }
15786
+ if (!message || !message.trim()) {
15787
+ console.error(chalk9.red("--message cannot be empty."));
15788
+ process.exit(1);
15789
+ }
15790
+ const results = [];
15791
+ for (let i = 0;i < targets.length; i++) {
15792
+ const target = targets[i];
15793
+ if (i > 0 && stagger > 0)
15794
+ await sleep(stagger);
15795
+ try {
15796
+ const result = await tmuxSend(target, message, {
15797
+ delayMs: Number.isFinite(opts.delay) ? opts.delay : undefined,
15798
+ retries: Number.isFinite(opts.retries) ? opts.retries : undefined,
15799
+ verify: opts.verify !== false
15800
+ });
15801
+ results.push({ target, ...result });
15802
+ if (!opts.json) {
15803
+ if (result.success) {
15804
+ console.log(chalk9.green(` \u2713 ${target}`) + chalk9.dim(` (attempt ${result.attempts})`));
15805
+ } else {
15806
+ console.log(chalk9.red(` \u2717 ${target}`) + chalk9.dim(` (failed after ${result.attempts} attempts)`));
15807
+ }
15808
+ }
15809
+ } catch (err) {
15810
+ const errMsg = err instanceof Error ? err.message : String(err);
15811
+ results.push({ target, success: false, attempts: 0, error: errMsg });
15812
+ if (!opts.json) {
15813
+ console.log(chalk9.red(` \u2717 ${target}: ${errMsg}`));
15814
+ }
15815
+ }
15816
+ }
15817
+ const succeeded = results.filter((r) => r.success).length;
15818
+ const failed = results.length - succeeded;
15819
+ if (opts.json) {
15820
+ console.log(JSON.stringify({ results, succeeded, failed, total: results.length }));
15821
+ } else {
15822
+ console.log(chalk9.dim(`
15823
+ Broadcast complete: ${chalk9.green(succeeded)} succeeded, ${failed > 0 ? chalk9.red(failed) : chalk9.dim(failed)} failed`));
15824
+ }
15825
+ if (failed > 0)
15826
+ process.exit(1);
15827
+ });
15828
+ }
15829
+ var init_tmux = () => {};
15830
+
15712
15831
  // node_modules/zod/v3/helpers/util.js
15713
15832
  var util2, objectUtil2, ZodParsedType2, getParsedType2 = (data) => {
15714
15833
  const t = typeof data;
@@ -46770,6 +46889,156 @@ var init_cloud = __esm(() => {
46770
46889
  CONFLICT_TABLES = new Set(["spaces", "projects", "agent_presence"]);
46771
46890
  });
46772
46891
 
46892
+ // src/mcp/channel.ts
46893
+ function registerChannelBridge(server, getAgentId) {
46894
+ server.server.registerCapabilities({
46895
+ experimental: {
46896
+ "claude/channel": {}
46897
+ }
46898
+ });
46899
+ let lastSeenId = 0;
46900
+ let pollTimer = null;
46901
+ function seedLastSeen(agentId) {
46902
+ const latest = readMessages({
46903
+ to: agentId,
46904
+ order: "desc",
46905
+ limit: 1
46906
+ });
46907
+ if (latest.length > 0) {
46908
+ lastSeenId = latest[0].id;
46909
+ }
46910
+ }
46911
+ function startPolling2() {
46912
+ if (pollTimer)
46913
+ return;
46914
+ const agentId = getAgentId();
46915
+ if (!agentId)
46916
+ return;
46917
+ seedLastSeen(agentId);
46918
+ pollTimer = setInterval(() => {
46919
+ const currentAgent = getAgentId();
46920
+ if (!currentAgent)
46921
+ return;
46922
+ try {
46923
+ const newMessages = readMessages({
46924
+ to: currentAgent,
46925
+ order: "asc",
46926
+ limit: 20
46927
+ }).filter((m) => m.id > lastSeenId);
46928
+ for (const msg of newMessages) {
46929
+ lastSeenId = msg.id;
46930
+ server.server.notification({
46931
+ method: "notifications/claude/channel",
46932
+ params: {
46933
+ content: msg.content,
46934
+ meta: {
46935
+ from: msg.from_agent,
46936
+ session_id: msg.session_id,
46937
+ ...msg.space ? { space: msg.space } : {},
46938
+ ...msg.priority !== "normal" ? { priority: msg.priority } : {}
46939
+ }
46940
+ }
46941
+ });
46942
+ }
46943
+ } catch {}
46944
+ }, POLL_INTERVAL_MS);
46945
+ }
46946
+ setTimeout(() => startPolling2(), 500);
46947
+ }
46948
+ var POLL_INTERVAL_MS = 1000;
46949
+ var init_channel = __esm(() => {
46950
+ init_messages();
46951
+ });
46952
+
46953
+ // src/mcp/tools/tmux.ts
46954
+ function sleep2(ms) {
46955
+ return new Promise((resolve) => setTimeout(resolve, ms));
46956
+ }
46957
+ function registerTmuxTools(server) {
46958
+ server.registerTool("tmux_send", {
46959
+ description: "Send a message to a tmux window (e.g. another agent's Claude Code session). " + "Pastes the text literally, waits for the pane to be idle, hits Enter, then verifies the message was submitted. " + "Retries up to N times on failure.",
46960
+ inputSchema: {
46961
+ target: exports_external2.string().describe("Tmux target: session:window or session:window.pane (e.g. platform-alumia:1)"),
46962
+ message: exports_external2.string().describe("Message text to send"),
46963
+ delay_ms: exports_external2.coerce.number().optional().describe("Wait time (ms) after paste before hitting Enter. Default: max(12000, message_length * 50)"),
46964
+ retries: exports_external2.coerce.number().optional().describe("Max retry attempts (default: 3)"),
46965
+ verify: exports_external2.coerce.boolean().optional().describe("Verify message was submitted after Enter (default: true)")
46966
+ }
46967
+ }, async (args) => {
46968
+ const { target, message, delay_ms, retries, verify } = args;
46969
+ if (!target || !target.trim()) {
46970
+ return { content: [{ type: "text", text: "target is required" }], isError: true };
46971
+ }
46972
+ if (!message || !message.trim()) {
46973
+ return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
46974
+ }
46975
+ try {
46976
+ const result = await tmuxSend(target.trim(), message, {
46977
+ delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
46978
+ retries: typeof retries === "number" && retries > 0 ? retries : undefined,
46979
+ verify: verify !== false
46980
+ });
46981
+ return {
46982
+ content: [{ type: "text", text: JSON.stringify({ target, result }) }],
46983
+ isError: !result.success
46984
+ };
46985
+ } catch (err) {
46986
+ const msg = err instanceof Error ? err.message : String(err);
46987
+ return { content: [{ type: "text", text: `tmux error: ${msg}` }], isError: true };
46988
+ }
46989
+ });
46990
+ server.registerTool("tmux_broadcast", {
46991
+ description: "Send the same message to multiple tmux windows simultaneously. " + "Useful for broadcasting instructions to several agent sessions at once. " + "Supports staggered sending and per-target retry.",
46992
+ inputSchema: {
46993
+ targets: exports_external2.array(exports_external2.string()).describe("List of tmux targets (session:window or session:window.pane)"),
46994
+ message: exports_external2.string().describe("Message text to send to all targets"),
46995
+ delay_ms: exports_external2.coerce.number().optional().describe("Wait time (ms) after paste before Enter. Default: max(12000, message_length * 50)"),
46996
+ stagger_ms: exports_external2.coerce.number().optional().describe("Delay (ms) between sending to each target (default: 0)"),
46997
+ retries: exports_external2.coerce.number().optional().describe("Max retry attempts per target (default: 3)"),
46998
+ verify: exports_external2.coerce.boolean().optional().describe("Verify each message was submitted (default: true)")
46999
+ }
47000
+ }, async (args) => {
47001
+ const { targets, message, delay_ms, stagger_ms, retries, verify } = args;
47002
+ if (!Array.isArray(targets) || targets.length === 0) {
47003
+ return { content: [{ type: "text", text: "targets must be a non-empty array" }], isError: true };
47004
+ }
47005
+ if (!message || !message.trim()) {
47006
+ return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
47007
+ }
47008
+ const stagger = typeof stagger_ms === "number" && stagger_ms > 0 ? stagger_ms : 0;
47009
+ const results = [];
47010
+ for (let i = 0;i < targets.length; i++) {
47011
+ const target = targets[i].trim();
47012
+ if (i > 0 && stagger > 0)
47013
+ await sleep2(stagger);
47014
+ try {
47015
+ const result = await tmuxSend(target, message, {
47016
+ delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
47017
+ retries: typeof retries === "number" && retries > 0 ? retries : undefined,
47018
+ verify: verify !== false
47019
+ });
47020
+ results.push({ target, ...result });
47021
+ } catch (err) {
47022
+ const errMsg = err instanceof Error ? err.message : String(err);
47023
+ results.push({ target, success: false, attempts: 0, error: errMsg });
47024
+ }
47025
+ }
47026
+ const succeeded = results.filter((r) => r.success).length;
47027
+ const failed = results.length - succeeded;
47028
+ return {
47029
+ content: [{
47030
+ type: "text",
47031
+ text: JSON.stringify({ results, succeeded, failed, total: results.length })
47032
+ }],
47033
+ isError: failed > 0
47034
+ };
47035
+ });
47036
+ }
47037
+ var init_tmux2 = __esm(() => {
47038
+ init_zod2();
47039
+ init_tmux();
47040
+ });
47041
+
46773
47042
  // src/mcp/index.ts
46774
47043
  var exports_mcp = {};
46775
47044
  __export(exports_mcp, {
@@ -46804,6 +47073,8 @@ var init_mcp2 = __esm(() => {
46804
47073
  init_agents();
46805
47074
  init_advanced();
46806
47075
  init_cloud();
47076
+ init_channel();
47077
+ init_tmux2();
46807
47078
  import__package2 = __toESM(require_package(), 1);
46808
47079
  server = new McpServer({
46809
47080
  name: "conversations",
@@ -46815,6 +47086,8 @@ var init_mcp2 = __esm(() => {
46815
47086
  registerProjectTools(server);
46816
47087
  registerAgentTools(server, agentFocus, getAgentFocus);
46817
47088
  registerAdvancedTools(server, import__package2.default.version);
47089
+ registerTmuxTools(server);
47090
+ registerChannelBridge(server, () => process.env.CONVERSATIONS_AGENT_ID ?? null);
46818
47091
  isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
46819
47092
  if (isDirectRun) {
46820
47093
  startMcpServer().catch((error48) => {
@@ -47403,7 +47676,7 @@ var {
47403
47676
 
47404
47677
  // src/cli/index.tsx
47405
47678
  init_identity();
47406
- import chalk9 from "chalk";
47679
+ import chalk10 from "chalk";
47407
47680
  import { render } from "ink";
47408
47681
  import React8 from "react";
47409
47682
 
@@ -50016,6 +50289,7 @@ function registerAnalyticsCommands(program2) {
50016
50289
  }
50017
50290
 
50018
50291
  // src/cli/index.tsx
50292
+ init_tmux();
50019
50293
  var import__package3 = __toESM(require_package(), 1);
50020
50294
  var program2 = new Command;
50021
50295
  program2.name("conversations").description("Real-time CLI messaging for AI agents").version(import__package3.default.version);
@@ -50024,6 +50298,7 @@ registerSpaceCommands(program2);
50024
50298
  registerProjectCommands(program2);
50025
50299
  registerAgentCommands(program2);
50026
50300
  registerAnalyticsCommands(program2);
50301
+ registerTmuxCommands(program2);
50027
50302
  program2.command("mcp").description("Start MCP server").action(async () => {
50028
50303
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
50029
50304
  await startMcpServer2();
@@ -50048,11 +50323,11 @@ registerCloudCommands(program2, "conversations");
50048
50323
  const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
50049
50324
  const config2 = getCloudConfig2();
50050
50325
  if (config2.mode === "local") {
50051
- console.error(chalk9.red("Error: cloud mode not configured. Set RDS credentials first."));
50326
+ console.error(chalk10.red("Error: cloud mode not configured. Set RDS credentials first."));
50052
50327
  process.exit(1);
50053
50328
  }
50054
50329
  if (opts.dryRun) {
50055
- console.log(chalk9.dim(`-- Dry run: SQL that would be executed --
50330
+ console.log(chalk10.dim(`-- Dry run: SQL that would be executed --
50056
50331
  `));
50057
50332
  for (const sql of PG_MIGRATIONS2)
50058
50333
  console.log(sql);
@@ -50060,14 +50335,14 @@ registerCloudCommands(program2, "conversations");
50060
50335
  }
50061
50336
  const pg = new PgAdapterAsync2(getConnectionString2("conversations"));
50062
50337
  for (let i = 0;i < PG_MIGRATIONS2.length; i++) {
50063
- process.stdout.write(chalk9.dim(`Running migration ${i + 1}/${PG_MIGRATIONS2.length}...`));
50338
+ process.stdout.write(chalk10.dim(`Running migration ${i + 1}/${PG_MIGRATIONS2.length}...`));
50064
50339
  await pg.run(PG_MIGRATIONS2[i]);
50065
- console.log(chalk9.green(" done"));
50340
+ console.log(chalk10.green(" done"));
50066
50341
  }
50067
50342
  await pg.close();
50068
- console.log(chalk9.green("\u2713 All migrations applied."));
50343
+ console.log(chalk10.green("\u2713 All migrations applied."));
50069
50344
  } catch (e) {
50070
- console.error(chalk9.red(`Migration failed: ${e?.message ?? e}`));
50345
+ console.error(chalk10.red(`Migration failed: ${e?.message ?? e}`));
50071
50346
  process.exit(1);
50072
50347
  }
50073
50348
  });
@@ -50075,8 +50350,8 @@ registerCloudCommands(program2, "conversations");
50075
50350
  }
50076
50351
  program2.action(() => {
50077
50352
  if (!process.stdin.isTTY) {
50078
- console.error(chalk9.red("Interactive mode requires a TTY terminal."));
50079
- console.error(chalk9.dim("Use subcommands (send, read, sessions, etc.) for non-interactive use."));
50353
+ console.error(chalk10.red("Interactive mode requires a TTY terminal."));
50354
+ console.error(chalk10.dim("Use subcommands (send, read, sessions, etc.) for non-interactive use."));
50080
50355
  process.exit(1);
50081
50356
  }
50082
50357
  const agent = resolveIdentity();
package/bin/mcp.js CHANGED
@@ -44049,10 +44049,182 @@ function formatError2(e) {
44049
44049
  return e.message;
44050
44050
  return String(e);
44051
44051
  }
44052
+
44053
+ // src/mcp/channel.ts
44054
+ var POLL_INTERVAL_MS = 1000;
44055
+ function registerChannelBridge(server, getAgentId) {
44056
+ server.server.registerCapabilities({
44057
+ experimental: {
44058
+ "claude/channel": {}
44059
+ }
44060
+ });
44061
+ let lastSeenId = 0;
44062
+ let pollTimer = null;
44063
+ function seedLastSeen(agentId) {
44064
+ const latest = readMessages({
44065
+ to: agentId,
44066
+ order: "desc",
44067
+ limit: 1
44068
+ });
44069
+ if (latest.length > 0) {
44070
+ lastSeenId = latest[0].id;
44071
+ }
44072
+ }
44073
+ function startPolling() {
44074
+ if (pollTimer)
44075
+ return;
44076
+ const agentId = getAgentId();
44077
+ if (!agentId)
44078
+ return;
44079
+ seedLastSeen(agentId);
44080
+ pollTimer = setInterval(() => {
44081
+ const currentAgent = getAgentId();
44082
+ if (!currentAgent)
44083
+ return;
44084
+ try {
44085
+ const newMessages = readMessages({
44086
+ to: currentAgent,
44087
+ order: "asc",
44088
+ limit: 20
44089
+ }).filter((m) => m.id > lastSeenId);
44090
+ for (const msg of newMessages) {
44091
+ lastSeenId = msg.id;
44092
+ server.server.notification({
44093
+ method: "notifications/claude/channel",
44094
+ params: {
44095
+ content: msg.content,
44096
+ meta: {
44097
+ from: msg.from_agent,
44098
+ session_id: msg.session_id,
44099
+ ...msg.space ? { space: msg.space } : {},
44100
+ ...msg.priority !== "normal" ? { priority: msg.priority } : {}
44101
+ }
44102
+ }
44103
+ });
44104
+ }
44105
+ } catch {}
44106
+ }, POLL_INTERVAL_MS);
44107
+ }
44108
+ setTimeout(() => startPolling(), 500);
44109
+ }
44110
+
44111
+ // src/cli/commands/tmux.ts
44112
+ import { execSync } from "child_process";
44113
+ function sleep(ms) {
44114
+ return new Promise((resolve) => setTimeout(resolve, ms));
44115
+ }
44116
+ async function tmuxSend(target, message, opts = {}) {
44117
+ const delay = opts.delayMs ?? Math.max(12000, message.length * 50);
44118
+ const maxRetries = opts.retries ?? 3;
44119
+ const verify = opts.verify !== false;
44120
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
44121
+ execSync(`tmux send-keys -t ${JSON.stringify(target)} -l ${JSON.stringify(message)}`);
44122
+ await sleep(delay);
44123
+ execSync(`tmux send-keys -t ${JSON.stringify(target)} Enter`);
44124
+ if (!verify)
44125
+ return { success: true, attempts: attempt };
44126
+ await sleep(500);
44127
+ const pane = execSync(`tmux capture-pane -t ${JSON.stringify(target)} -p`).toString();
44128
+ const lastLines = pane.split(`
44129
+ `).slice(-3).join(`
44130
+ `);
44131
+ if (!lastLines.includes(message.slice(0, 20))) {
44132
+ return { success: true, attempts: attempt };
44133
+ }
44134
+ if (attempt < maxRetries)
44135
+ await sleep(2000);
44136
+ }
44137
+ return { success: false, attempts: maxRetries };
44138
+ }
44139
+
44140
+ // src/mcp/tools/tmux.ts
44141
+ function sleep2(ms) {
44142
+ return new Promise((resolve) => setTimeout(resolve, ms));
44143
+ }
44144
+ function registerTmuxTools(server) {
44145
+ server.registerTool("tmux_send", {
44146
+ description: "Send a message to a tmux window (e.g. another agent's Claude Code session). " + "Pastes the text literally, waits for the pane to be idle, hits Enter, then verifies the message was submitted. " + "Retries up to N times on failure.",
44147
+ inputSchema: {
44148
+ target: exports_external.string().describe("Tmux target: session:window or session:window.pane (e.g. platform-alumia:1)"),
44149
+ message: exports_external.string().describe("Message text to send"),
44150
+ delay_ms: exports_external.coerce.number().optional().describe("Wait time (ms) after paste before hitting Enter. Default: max(12000, message_length * 50)"),
44151
+ retries: exports_external.coerce.number().optional().describe("Max retry attempts (default: 3)"),
44152
+ verify: exports_external.coerce.boolean().optional().describe("Verify message was submitted after Enter (default: true)")
44153
+ }
44154
+ }, async (args) => {
44155
+ const { target, message, delay_ms, retries, verify } = args;
44156
+ if (!target || !target.trim()) {
44157
+ return { content: [{ type: "text", text: "target is required" }], isError: true };
44158
+ }
44159
+ if (!message || !message.trim()) {
44160
+ return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
44161
+ }
44162
+ try {
44163
+ const result = await tmuxSend(target.trim(), message, {
44164
+ delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
44165
+ retries: typeof retries === "number" && retries > 0 ? retries : undefined,
44166
+ verify: verify !== false
44167
+ });
44168
+ return {
44169
+ content: [{ type: "text", text: JSON.stringify({ target, result }) }],
44170
+ isError: !result.success
44171
+ };
44172
+ } catch (err) {
44173
+ const msg = err instanceof Error ? err.message : String(err);
44174
+ return { content: [{ type: "text", text: `tmux error: ${msg}` }], isError: true };
44175
+ }
44176
+ });
44177
+ server.registerTool("tmux_broadcast", {
44178
+ description: "Send the same message to multiple tmux windows simultaneously. " + "Useful for broadcasting instructions to several agent sessions at once. " + "Supports staggered sending and per-target retry.",
44179
+ inputSchema: {
44180
+ targets: exports_external.array(exports_external.string()).describe("List of tmux targets (session:window or session:window.pane)"),
44181
+ message: exports_external.string().describe("Message text to send to all targets"),
44182
+ delay_ms: exports_external.coerce.number().optional().describe("Wait time (ms) after paste before Enter. Default: max(12000, message_length * 50)"),
44183
+ stagger_ms: exports_external.coerce.number().optional().describe("Delay (ms) between sending to each target (default: 0)"),
44184
+ retries: exports_external.coerce.number().optional().describe("Max retry attempts per target (default: 3)"),
44185
+ verify: exports_external.coerce.boolean().optional().describe("Verify each message was submitted (default: true)")
44186
+ }
44187
+ }, async (args) => {
44188
+ const { targets, message, delay_ms, stagger_ms, retries, verify } = args;
44189
+ if (!Array.isArray(targets) || targets.length === 0) {
44190
+ return { content: [{ type: "text", text: "targets must be a non-empty array" }], isError: true };
44191
+ }
44192
+ if (!message || !message.trim()) {
44193
+ return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
44194
+ }
44195
+ const stagger = typeof stagger_ms === "number" && stagger_ms > 0 ? stagger_ms : 0;
44196
+ const results = [];
44197
+ for (let i = 0;i < targets.length; i++) {
44198
+ const target = targets[i].trim();
44199
+ if (i > 0 && stagger > 0)
44200
+ await sleep2(stagger);
44201
+ try {
44202
+ const result = await tmuxSend(target, message, {
44203
+ delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
44204
+ retries: typeof retries === "number" && retries > 0 ? retries : undefined,
44205
+ verify: verify !== false
44206
+ });
44207
+ results.push({ target, ...result });
44208
+ } catch (err) {
44209
+ const errMsg = err instanceof Error ? err.message : String(err);
44210
+ results.push({ target, success: false, attempts: 0, error: errMsg });
44211
+ }
44212
+ }
44213
+ const succeeded = results.filter((r) => r.success).length;
44214
+ const failed = results.length - succeeded;
44215
+ return {
44216
+ content: [{
44217
+ type: "text",
44218
+ text: JSON.stringify({ results, succeeded, failed, total: results.length })
44219
+ }],
44220
+ isError: failed > 0
44221
+ };
44222
+ });
44223
+ }
44052
44224
  // package.json
44053
44225
  var package_default = {
44054
44226
  name: "@hasna/conversations",
44055
- version: "0.2.29",
44227
+ version: "0.2.30",
44056
44228
  description: "Real-time CLI messaging for AI agents",
44057
44229
  type: "module",
44058
44230
  bin: {
@@ -44154,6 +44326,8 @@ registerSpaceTools(server);
44154
44326
  registerProjectTools(server);
44155
44327
  registerAgentTools(server, agentFocus, getAgentFocus);
44156
44328
  registerAdvancedTools(server, package_default.version);
44329
+ registerTmuxTools(server);
44330
+ registerChannelBridge(server, () => process.env.CONVERSATIONS_AGENT_ID ?? null);
44157
44331
  async function startMcpServer() {
44158
44332
  const transport = new StdioServerTransport;
44159
44333
  registerCloudSyncTools(server);
@@ -0,0 +1,13 @@
1
+ import type { Command } from "commander";
2
+ interface TmuxSendOptions {
3
+ delayMs?: number;
4
+ retries?: number;
5
+ verify?: boolean;
6
+ }
7
+ interface TmuxSendResult {
8
+ success: boolean;
9
+ attempts: number;
10
+ }
11
+ export declare function tmuxSend(target: string, message: string, opts?: TmuxSendOptions): Promise<TmuxSendResult>;
12
+ export declare function registerTmuxCommands(program: Command): void;
13
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Claude Code channel bridge for conversations MCP server.
3
+ *
4
+ * Declares `experimental['claude/channel']` capability so Claude Code
5
+ * (and agent-claude) can use this server as a channel for inter-session
6
+ * messaging. When enabled, polls for new DMs to the current agent and
7
+ * pushes them as `notifications/claude/channel` events.
8
+ *
9
+ * Usage: Start agent-claude with:
10
+ * agent-claude --channels server:conversations --dangerously-load-development-channels
11
+ */
12
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ /**
14
+ * Register the claude/channel capability and start polling for
15
+ * inbound messages to push as notifications.
16
+ */
17
+ export declare function registerChannelBridge(server: McpServer, getAgentId: () => string | null): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tmux dispatch tools: tmux_send, tmux_broadcast
3
+ *
4
+ * Send messages to tmux windows (other Claude Code sessions) with
5
+ * smart paste → wait → Enter → verify behavior.
6
+ */
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ export declare function registerTmuxTools(server: McpServer): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {