@hasna/conversations 0.2.29 → 0.2.31
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 +300 -10
- package/bin/mcp.js +191 -1
- package/dist/cli/commands/tmux.d.ts +13 -0
- package/dist/mcp/channel.d.ts +17 -0
- package/dist/mcp/tools/tmux.d.ts +8 -0
- package/package.json +2 -2
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.
|
|
14931
|
+
version: "0.2.31",
|
|
14932
14932
|
description: "Real-time CLI messaging for AI agents",
|
|
14933
14933
|
type: "module",
|
|
14934
14934
|
bin: {
|
|
@@ -15709,6 +15709,140 @@ 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
|
+
function countLines(message) {
|
|
15719
|
+
const matches = message.match(/\r?\n/g);
|
|
15720
|
+
return (matches?.length ?? 0) + 1;
|
|
15721
|
+
}
|
|
15722
|
+
function getDefaultDelayMs(message) {
|
|
15723
|
+
const byLength = message.length * 1.5;
|
|
15724
|
+
const byLines = countLines(message) * 10;
|
|
15725
|
+
return Math.max(25, Math.min(1500, Math.round(byLength + byLines)));
|
|
15726
|
+
}
|
|
15727
|
+
function getVerifyPauseMs(message) {
|
|
15728
|
+
return message.length <= 120 ? 50 : 100;
|
|
15729
|
+
}
|
|
15730
|
+
function getRetryBackoffMs(attempt) {
|
|
15731
|
+
return Math.min(500, 100 * attempt);
|
|
15732
|
+
}
|
|
15733
|
+
async function tmuxSend(target, message, opts = {}) {
|
|
15734
|
+
const delay = opts.delayMs ?? getDefaultDelayMs(message);
|
|
15735
|
+
const maxRetries = opts.retries ?? 3;
|
|
15736
|
+
const verify = opts.verify !== false;
|
|
15737
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
15738
|
+
execSync(`tmux send-keys -t ${JSON.stringify(target)} -l ${JSON.stringify(message)}`);
|
|
15739
|
+
await sleep(delay);
|
|
15740
|
+
execSync(`tmux send-keys -t ${JSON.stringify(target)} Enter`);
|
|
15741
|
+
if (!verify)
|
|
15742
|
+
return { success: true, attempts: attempt };
|
|
15743
|
+
await sleep(getVerifyPauseMs(message));
|
|
15744
|
+
const pane = execSync(`tmux capture-pane -t ${JSON.stringify(target)} -p`).toString();
|
|
15745
|
+
const lastLines = pane.split(`
|
|
15746
|
+
`).slice(-6).join(`
|
|
15747
|
+
`);
|
|
15748
|
+
const marker = message.slice(0, Math.min(32, message.length));
|
|
15749
|
+
if (!lastLines.includes(marker)) {
|
|
15750
|
+
return { success: true, attempts: attempt };
|
|
15751
|
+
}
|
|
15752
|
+
if (attempt < maxRetries)
|
|
15753
|
+
await sleep(getRetryBackoffMs(attempt));
|
|
15754
|
+
}
|
|
15755
|
+
return { success: false, attempts: maxRetries };
|
|
15756
|
+
}
|
|
15757
|
+
function registerTmuxCommands(program2) {
|
|
15758
|
+
const tmux = program2.command("tmux").description("Dispatch messages to tmux windows (Claude Code sessions)");
|
|
15759
|
+
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: adaptive 25-1500ms)", 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) => {
|
|
15760
|
+
const target = opts.target.trim();
|
|
15761
|
+
const message = opts.message;
|
|
15762
|
+
if (!target) {
|
|
15763
|
+
console.error(chalk9.red("--target is required."));
|
|
15764
|
+
process.exit(1);
|
|
15765
|
+
}
|
|
15766
|
+
if (!message || !message.trim()) {
|
|
15767
|
+
console.error(chalk9.red("--message cannot be empty."));
|
|
15768
|
+
process.exit(1);
|
|
15769
|
+
}
|
|
15770
|
+
try {
|
|
15771
|
+
const result = await tmuxSend(target, message, {
|
|
15772
|
+
delayMs: Number.isFinite(opts.delay) ? opts.delay : undefined,
|
|
15773
|
+
retries: Number.isFinite(opts.retries) ? opts.retries : undefined,
|
|
15774
|
+
verify: opts.verify !== false
|
|
15775
|
+
});
|
|
15776
|
+
if (opts.json) {
|
|
15777
|
+
console.log(JSON.stringify({ target, result }));
|
|
15778
|
+
} else if (result.success) {
|
|
15779
|
+
console.log(chalk9.green(`Sent to ${target}`) + chalk9.dim(` (attempt ${result.attempts})`));
|
|
15780
|
+
} else {
|
|
15781
|
+
console.error(chalk9.red(`Failed to confirm delivery to ${target}`) + chalk9.dim(` after ${result.attempts} attempt(s)`));
|
|
15782
|
+
process.exit(1);
|
|
15783
|
+
}
|
|
15784
|
+
} catch (err) {
|
|
15785
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
15786
|
+
if (opts.json) {
|
|
15787
|
+
console.log(JSON.stringify({ target, error: msg }));
|
|
15788
|
+
} else {
|
|
15789
|
+
console.error(chalk9.red(`tmux error: ${msg}`));
|
|
15790
|
+
}
|
|
15791
|
+
process.exit(1);
|
|
15792
|
+
}
|
|
15793
|
+
});
|
|
15794
|
+
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: adaptive 25-1500ms)", parseInt).option("--stagger <ms>", "Delay (ms) between each target (default: 500)", 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) => {
|
|
15795
|
+
const targets = opts.targets.split(",").map((t) => t.trim()).filter(Boolean);
|
|
15796
|
+
const message = opts.message;
|
|
15797
|
+
const stagger = Number.isFinite(opts.stagger) && opts.stagger >= 0 ? opts.stagger : 500;
|
|
15798
|
+
if (targets.length === 0) {
|
|
15799
|
+
console.error(chalk9.red("--targets must be a non-empty comma-separated list."));
|
|
15800
|
+
process.exit(1);
|
|
15801
|
+
}
|
|
15802
|
+
if (!message || !message.trim()) {
|
|
15803
|
+
console.error(chalk9.red("--message cannot be empty."));
|
|
15804
|
+
process.exit(1);
|
|
15805
|
+
}
|
|
15806
|
+
const results = new Array(targets.length);
|
|
15807
|
+
await Promise.all(targets.map(async (target, i) => {
|
|
15808
|
+
if (i > 0 && stagger > 0)
|
|
15809
|
+
await sleep(stagger * i);
|
|
15810
|
+
try {
|
|
15811
|
+
const result = await tmuxSend(target, message, {
|
|
15812
|
+
delayMs: Number.isFinite(opts.delay) ? opts.delay : undefined,
|
|
15813
|
+
retries: Number.isFinite(opts.retries) ? opts.retries : undefined,
|
|
15814
|
+
verify: opts.verify !== false
|
|
15815
|
+
});
|
|
15816
|
+
results[i] = { target, ...result };
|
|
15817
|
+
if (!opts.json) {
|
|
15818
|
+
if (result.success) {
|
|
15819
|
+
console.log(chalk9.green(` \u2713 ${target}`) + chalk9.dim(` (attempt ${result.attempts})`));
|
|
15820
|
+
} else {
|
|
15821
|
+
console.log(chalk9.red(` \u2717 ${target}`) + chalk9.dim(` (failed after ${result.attempts} attempts)`));
|
|
15822
|
+
}
|
|
15823
|
+
}
|
|
15824
|
+
} catch (err) {
|
|
15825
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
15826
|
+
results[i] = { target, success: false, attempts: 0, error: errMsg };
|
|
15827
|
+
if (!opts.json) {
|
|
15828
|
+
console.log(chalk9.red(` \u2717 ${target}: ${errMsg}`));
|
|
15829
|
+
}
|
|
15830
|
+
}
|
|
15831
|
+
}));
|
|
15832
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
15833
|
+
const failed = results.length - succeeded;
|
|
15834
|
+
if (opts.json) {
|
|
15835
|
+
console.log(JSON.stringify({ results, succeeded, failed, total: results.length }));
|
|
15836
|
+
} else {
|
|
15837
|
+
console.log(chalk9.dim(`
|
|
15838
|
+
Broadcast complete: ${chalk9.green(succeeded)} succeeded, ${failed > 0 ? chalk9.red(failed) : chalk9.dim(failed)} failed`));
|
|
15839
|
+
}
|
|
15840
|
+
if (failed > 0)
|
|
15841
|
+
process.exit(1);
|
|
15842
|
+
});
|
|
15843
|
+
}
|
|
15844
|
+
var init_tmux = () => {};
|
|
15845
|
+
|
|
15712
15846
|
// node_modules/zod/v3/helpers/util.js
|
|
15713
15847
|
var util2, objectUtil2, ZodParsedType2, getParsedType2 = (data) => {
|
|
15714
15848
|
const t = typeof data;
|
|
@@ -46770,6 +46904,156 @@ var init_cloud = __esm(() => {
|
|
|
46770
46904
|
CONFLICT_TABLES = new Set(["spaces", "projects", "agent_presence"]);
|
|
46771
46905
|
});
|
|
46772
46906
|
|
|
46907
|
+
// src/mcp/channel.ts
|
|
46908
|
+
function registerChannelBridge(server, getAgentId) {
|
|
46909
|
+
server.server.registerCapabilities({
|
|
46910
|
+
experimental: {
|
|
46911
|
+
"claude/channel": {}
|
|
46912
|
+
}
|
|
46913
|
+
});
|
|
46914
|
+
let lastSeenId = 0;
|
|
46915
|
+
let pollTimer = null;
|
|
46916
|
+
function seedLastSeen(agentId) {
|
|
46917
|
+
const latest = readMessages({
|
|
46918
|
+
to: agentId,
|
|
46919
|
+
order: "desc",
|
|
46920
|
+
limit: 1
|
|
46921
|
+
});
|
|
46922
|
+
if (latest.length > 0) {
|
|
46923
|
+
lastSeenId = latest[0].id;
|
|
46924
|
+
}
|
|
46925
|
+
}
|
|
46926
|
+
function startPolling2() {
|
|
46927
|
+
if (pollTimer)
|
|
46928
|
+
return;
|
|
46929
|
+
const agentId = getAgentId();
|
|
46930
|
+
if (!agentId)
|
|
46931
|
+
return;
|
|
46932
|
+
seedLastSeen(agentId);
|
|
46933
|
+
pollTimer = setInterval(() => {
|
|
46934
|
+
const currentAgent = getAgentId();
|
|
46935
|
+
if (!currentAgent)
|
|
46936
|
+
return;
|
|
46937
|
+
try {
|
|
46938
|
+
const newMessages = readMessages({
|
|
46939
|
+
to: currentAgent,
|
|
46940
|
+
order: "asc",
|
|
46941
|
+
limit: 20
|
|
46942
|
+
}).filter((m) => m.id > lastSeenId);
|
|
46943
|
+
for (const msg of newMessages) {
|
|
46944
|
+
lastSeenId = msg.id;
|
|
46945
|
+
server.server.notification({
|
|
46946
|
+
method: "notifications/claude/channel",
|
|
46947
|
+
params: {
|
|
46948
|
+
content: msg.content,
|
|
46949
|
+
meta: {
|
|
46950
|
+
from: msg.from_agent,
|
|
46951
|
+
session_id: msg.session_id,
|
|
46952
|
+
...msg.space ? { space: msg.space } : {},
|
|
46953
|
+
...msg.priority !== "normal" ? { priority: msg.priority } : {}
|
|
46954
|
+
}
|
|
46955
|
+
}
|
|
46956
|
+
});
|
|
46957
|
+
}
|
|
46958
|
+
} catch {}
|
|
46959
|
+
}, POLL_INTERVAL_MS);
|
|
46960
|
+
}
|
|
46961
|
+
setTimeout(() => startPolling2(), 500);
|
|
46962
|
+
}
|
|
46963
|
+
var POLL_INTERVAL_MS = 1000;
|
|
46964
|
+
var init_channel = __esm(() => {
|
|
46965
|
+
init_messages();
|
|
46966
|
+
});
|
|
46967
|
+
|
|
46968
|
+
// src/mcp/tools/tmux.ts
|
|
46969
|
+
function sleep2(ms) {
|
|
46970
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46971
|
+
}
|
|
46972
|
+
function registerTmuxTools(server) {
|
|
46973
|
+
server.registerTool("tmux_send", {
|
|
46974
|
+
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.",
|
|
46975
|
+
inputSchema: {
|
|
46976
|
+
target: exports_external2.string().describe("Tmux target: session:window or session:window.pane (e.g. platform-alumia:1)"),
|
|
46977
|
+
message: exports_external2.string().describe("Message text to send"),
|
|
46978
|
+
delay_ms: exports_external2.coerce.number().optional().describe("Wait time (ms) after paste before hitting Enter. Default: adaptive 25-1500ms"),
|
|
46979
|
+
retries: exports_external2.coerce.number().optional().describe("Max retry attempts (default: 3)"),
|
|
46980
|
+
verify: exports_external2.coerce.boolean().optional().describe("Verify message was submitted after Enter (default: true)")
|
|
46981
|
+
}
|
|
46982
|
+
}, async (args) => {
|
|
46983
|
+
const { target, message, delay_ms, retries, verify } = args;
|
|
46984
|
+
if (!target || !target.trim()) {
|
|
46985
|
+
return { content: [{ type: "text", text: "target is required" }], isError: true };
|
|
46986
|
+
}
|
|
46987
|
+
if (!message || !message.trim()) {
|
|
46988
|
+
return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
|
|
46989
|
+
}
|
|
46990
|
+
try {
|
|
46991
|
+
const result = await tmuxSend(target.trim(), message, {
|
|
46992
|
+
delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
|
|
46993
|
+
retries: typeof retries === "number" && retries > 0 ? retries : undefined,
|
|
46994
|
+
verify: verify !== false
|
|
46995
|
+
});
|
|
46996
|
+
return {
|
|
46997
|
+
content: [{ type: "text", text: JSON.stringify({ target, result }) }],
|
|
46998
|
+
isError: !result.success
|
|
46999
|
+
};
|
|
47000
|
+
} catch (err) {
|
|
47001
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47002
|
+
return { content: [{ type: "text", text: `tmux error: ${msg}` }], isError: true };
|
|
47003
|
+
}
|
|
47004
|
+
});
|
|
47005
|
+
server.registerTool("tmux_broadcast", {
|
|
47006
|
+
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.",
|
|
47007
|
+
inputSchema: {
|
|
47008
|
+
targets: exports_external2.array(exports_external2.string()).describe("List of tmux targets (session:window or session:window.pane)"),
|
|
47009
|
+
message: exports_external2.string().describe("Message text to send to all targets"),
|
|
47010
|
+
delay_ms: exports_external2.coerce.number().optional().describe("Wait time (ms) after paste before Enter. Default: adaptive 25-1500ms"),
|
|
47011
|
+
stagger_ms: exports_external2.coerce.number().optional().describe("Delay (ms) between sending to each target (default: 500)"),
|
|
47012
|
+
retries: exports_external2.coerce.number().optional().describe("Max retry attempts per target (default: 3)"),
|
|
47013
|
+
verify: exports_external2.coerce.boolean().optional().describe("Verify each message was submitted (default: true)")
|
|
47014
|
+
}
|
|
47015
|
+
}, async (args) => {
|
|
47016
|
+
const { targets, message, delay_ms, stagger_ms, retries, verify } = args;
|
|
47017
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
47018
|
+
return { content: [{ type: "text", text: "targets must be a non-empty array" }], isError: true };
|
|
47019
|
+
}
|
|
47020
|
+
if (!message || !message.trim()) {
|
|
47021
|
+
return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
|
|
47022
|
+
}
|
|
47023
|
+
const stagger = typeof stagger_ms === "number" && stagger_ms >= 0 ? stagger_ms : 500;
|
|
47024
|
+
const results = new Array(targets.length);
|
|
47025
|
+
await Promise.all(targets.map(async (rawTarget, i) => {
|
|
47026
|
+
const target = String(rawTarget).trim();
|
|
47027
|
+
if (i > 0 && stagger > 0)
|
|
47028
|
+
await sleep2(stagger * i);
|
|
47029
|
+
try {
|
|
47030
|
+
const result = await tmuxSend(target, message, {
|
|
47031
|
+
delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
|
|
47032
|
+
retries: typeof retries === "number" && retries > 0 ? retries : undefined,
|
|
47033
|
+
verify: verify !== false
|
|
47034
|
+
});
|
|
47035
|
+
results[i] = { target, ...result };
|
|
47036
|
+
} catch (err) {
|
|
47037
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
47038
|
+
results[i] = { target, success: false, attempts: 0, error: errMsg };
|
|
47039
|
+
}
|
|
47040
|
+
}));
|
|
47041
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
47042
|
+
const failed = results.length - succeeded;
|
|
47043
|
+
return {
|
|
47044
|
+
content: [{
|
|
47045
|
+
type: "text",
|
|
47046
|
+
text: JSON.stringify({ results, succeeded, failed, total: results.length })
|
|
47047
|
+
}],
|
|
47048
|
+
isError: failed > 0
|
|
47049
|
+
};
|
|
47050
|
+
});
|
|
47051
|
+
}
|
|
47052
|
+
var init_tmux2 = __esm(() => {
|
|
47053
|
+
init_zod2();
|
|
47054
|
+
init_tmux();
|
|
47055
|
+
});
|
|
47056
|
+
|
|
46773
47057
|
// src/mcp/index.ts
|
|
46774
47058
|
var exports_mcp = {};
|
|
46775
47059
|
__export(exports_mcp, {
|
|
@@ -46804,6 +47088,8 @@ var init_mcp2 = __esm(() => {
|
|
|
46804
47088
|
init_agents();
|
|
46805
47089
|
init_advanced();
|
|
46806
47090
|
init_cloud();
|
|
47091
|
+
init_channel();
|
|
47092
|
+
init_tmux2();
|
|
46807
47093
|
import__package2 = __toESM(require_package(), 1);
|
|
46808
47094
|
server = new McpServer({
|
|
46809
47095
|
name: "conversations",
|
|
@@ -46815,6 +47101,8 @@ var init_mcp2 = __esm(() => {
|
|
|
46815
47101
|
registerProjectTools(server);
|
|
46816
47102
|
registerAgentTools(server, agentFocus, getAgentFocus);
|
|
46817
47103
|
registerAdvancedTools(server, import__package2.default.version);
|
|
47104
|
+
registerTmuxTools(server);
|
|
47105
|
+
registerChannelBridge(server, () => process.env.CONVERSATIONS_AGENT_ID ?? null);
|
|
46818
47106
|
isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
|
|
46819
47107
|
if (isDirectRun) {
|
|
46820
47108
|
startMcpServer().catch((error48) => {
|
|
@@ -47403,7 +47691,7 @@ var {
|
|
|
47403
47691
|
|
|
47404
47692
|
// src/cli/index.tsx
|
|
47405
47693
|
init_identity();
|
|
47406
|
-
import
|
|
47694
|
+
import chalk10 from "chalk";
|
|
47407
47695
|
import { render } from "ink";
|
|
47408
47696
|
import React8 from "react";
|
|
47409
47697
|
|
|
@@ -50016,6 +50304,7 @@ function registerAnalyticsCommands(program2) {
|
|
|
50016
50304
|
}
|
|
50017
50305
|
|
|
50018
50306
|
// src/cli/index.tsx
|
|
50307
|
+
init_tmux();
|
|
50019
50308
|
var import__package3 = __toESM(require_package(), 1);
|
|
50020
50309
|
var program2 = new Command;
|
|
50021
50310
|
program2.name("conversations").description("Real-time CLI messaging for AI agents").version(import__package3.default.version);
|
|
@@ -50024,6 +50313,7 @@ registerSpaceCommands(program2);
|
|
|
50024
50313
|
registerProjectCommands(program2);
|
|
50025
50314
|
registerAgentCommands(program2);
|
|
50026
50315
|
registerAnalyticsCommands(program2);
|
|
50316
|
+
registerTmuxCommands(program2);
|
|
50027
50317
|
program2.command("mcp").description("Start MCP server").action(async () => {
|
|
50028
50318
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
50029
50319
|
await startMcpServer2();
|
|
@@ -50048,11 +50338,11 @@ registerCloudCommands(program2, "conversations");
|
|
|
50048
50338
|
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
50049
50339
|
const config2 = getCloudConfig2();
|
|
50050
50340
|
if (config2.mode === "local") {
|
|
50051
|
-
console.error(
|
|
50341
|
+
console.error(chalk10.red("Error: cloud mode not configured. Set RDS credentials first."));
|
|
50052
50342
|
process.exit(1);
|
|
50053
50343
|
}
|
|
50054
50344
|
if (opts.dryRun) {
|
|
50055
|
-
console.log(
|
|
50345
|
+
console.log(chalk10.dim(`-- Dry run: SQL that would be executed --
|
|
50056
50346
|
`));
|
|
50057
50347
|
for (const sql of PG_MIGRATIONS2)
|
|
50058
50348
|
console.log(sql);
|
|
@@ -50060,14 +50350,14 @@ registerCloudCommands(program2, "conversations");
|
|
|
50060
50350
|
}
|
|
50061
50351
|
const pg = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
50062
50352
|
for (let i = 0;i < PG_MIGRATIONS2.length; i++) {
|
|
50063
|
-
process.stdout.write(
|
|
50353
|
+
process.stdout.write(chalk10.dim(`Running migration ${i + 1}/${PG_MIGRATIONS2.length}...`));
|
|
50064
50354
|
await pg.run(PG_MIGRATIONS2[i]);
|
|
50065
|
-
console.log(
|
|
50355
|
+
console.log(chalk10.green(" done"));
|
|
50066
50356
|
}
|
|
50067
50357
|
await pg.close();
|
|
50068
|
-
console.log(
|
|
50358
|
+
console.log(chalk10.green("\u2713 All migrations applied."));
|
|
50069
50359
|
} catch (e) {
|
|
50070
|
-
console.error(
|
|
50360
|
+
console.error(chalk10.red(`Migration failed: ${e?.message ?? e}`));
|
|
50071
50361
|
process.exit(1);
|
|
50072
50362
|
}
|
|
50073
50363
|
});
|
|
@@ -50075,8 +50365,8 @@ registerCloudCommands(program2, "conversations");
|
|
|
50075
50365
|
}
|
|
50076
50366
|
program2.action(() => {
|
|
50077
50367
|
if (!process.stdin.isTTY) {
|
|
50078
|
-
console.error(
|
|
50079
|
-
console.error(
|
|
50368
|
+
console.error(chalk10.red("Interactive mode requires a TTY terminal."));
|
|
50369
|
+
console.error(chalk10.dim("Use subcommands (send, read, sessions, etc.) for non-interactive use."));
|
|
50080
50370
|
process.exit(1);
|
|
50081
50371
|
}
|
|
50082
50372
|
const agent = resolveIdentity();
|
package/bin/mcp.js
CHANGED
|
@@ -44049,10 +44049,198 @@ 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
|
+
function countLines(message) {
|
|
44117
|
+
const matches = message.match(/\r?\n/g);
|
|
44118
|
+
return (matches?.length ?? 0) + 1;
|
|
44119
|
+
}
|
|
44120
|
+
function getDefaultDelayMs(message) {
|
|
44121
|
+
const byLength = message.length * 1.5;
|
|
44122
|
+
const byLines = countLines(message) * 10;
|
|
44123
|
+
return Math.max(25, Math.min(1500, Math.round(byLength + byLines)));
|
|
44124
|
+
}
|
|
44125
|
+
function getVerifyPauseMs(message) {
|
|
44126
|
+
return message.length <= 120 ? 50 : 100;
|
|
44127
|
+
}
|
|
44128
|
+
function getRetryBackoffMs(attempt) {
|
|
44129
|
+
return Math.min(500, 100 * attempt);
|
|
44130
|
+
}
|
|
44131
|
+
async function tmuxSend(target, message, opts = {}) {
|
|
44132
|
+
const delay = opts.delayMs ?? getDefaultDelayMs(message);
|
|
44133
|
+
const maxRetries = opts.retries ?? 3;
|
|
44134
|
+
const verify = opts.verify !== false;
|
|
44135
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
44136
|
+
execSync(`tmux send-keys -t ${JSON.stringify(target)} -l ${JSON.stringify(message)}`);
|
|
44137
|
+
await sleep(delay);
|
|
44138
|
+
execSync(`tmux send-keys -t ${JSON.stringify(target)} Enter`);
|
|
44139
|
+
if (!verify)
|
|
44140
|
+
return { success: true, attempts: attempt };
|
|
44141
|
+
await sleep(getVerifyPauseMs(message));
|
|
44142
|
+
const pane = execSync(`tmux capture-pane -t ${JSON.stringify(target)} -p`).toString();
|
|
44143
|
+
const lastLines = pane.split(`
|
|
44144
|
+
`).slice(-6).join(`
|
|
44145
|
+
`);
|
|
44146
|
+
const marker = message.slice(0, Math.min(32, message.length));
|
|
44147
|
+
if (!lastLines.includes(marker)) {
|
|
44148
|
+
return { success: true, attempts: attempt };
|
|
44149
|
+
}
|
|
44150
|
+
if (attempt < maxRetries)
|
|
44151
|
+
await sleep(getRetryBackoffMs(attempt));
|
|
44152
|
+
}
|
|
44153
|
+
return { success: false, attempts: maxRetries };
|
|
44154
|
+
}
|
|
44155
|
+
|
|
44156
|
+
// src/mcp/tools/tmux.ts
|
|
44157
|
+
function sleep2(ms) {
|
|
44158
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44159
|
+
}
|
|
44160
|
+
function registerTmuxTools(server) {
|
|
44161
|
+
server.registerTool("tmux_send", {
|
|
44162
|
+
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.",
|
|
44163
|
+
inputSchema: {
|
|
44164
|
+
target: exports_external.string().describe("Tmux target: session:window or session:window.pane (e.g. platform-alumia:1)"),
|
|
44165
|
+
message: exports_external.string().describe("Message text to send"),
|
|
44166
|
+
delay_ms: exports_external.coerce.number().optional().describe("Wait time (ms) after paste before hitting Enter. Default: adaptive 25-1500ms"),
|
|
44167
|
+
retries: exports_external.coerce.number().optional().describe("Max retry attempts (default: 3)"),
|
|
44168
|
+
verify: exports_external.coerce.boolean().optional().describe("Verify message was submitted after Enter (default: true)")
|
|
44169
|
+
}
|
|
44170
|
+
}, async (args) => {
|
|
44171
|
+
const { target, message, delay_ms, retries, verify } = args;
|
|
44172
|
+
if (!target || !target.trim()) {
|
|
44173
|
+
return { content: [{ type: "text", text: "target is required" }], isError: true };
|
|
44174
|
+
}
|
|
44175
|
+
if (!message || !message.trim()) {
|
|
44176
|
+
return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
|
|
44177
|
+
}
|
|
44178
|
+
try {
|
|
44179
|
+
const result = await tmuxSend(target.trim(), message, {
|
|
44180
|
+
delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
|
|
44181
|
+
retries: typeof retries === "number" && retries > 0 ? retries : undefined,
|
|
44182
|
+
verify: verify !== false
|
|
44183
|
+
});
|
|
44184
|
+
return {
|
|
44185
|
+
content: [{ type: "text", text: JSON.stringify({ target, result }) }],
|
|
44186
|
+
isError: !result.success
|
|
44187
|
+
};
|
|
44188
|
+
} catch (err) {
|
|
44189
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44190
|
+
return { content: [{ type: "text", text: `tmux error: ${msg}` }], isError: true };
|
|
44191
|
+
}
|
|
44192
|
+
});
|
|
44193
|
+
server.registerTool("tmux_broadcast", {
|
|
44194
|
+
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.",
|
|
44195
|
+
inputSchema: {
|
|
44196
|
+
targets: exports_external.array(exports_external.string()).describe("List of tmux targets (session:window or session:window.pane)"),
|
|
44197
|
+
message: exports_external.string().describe("Message text to send to all targets"),
|
|
44198
|
+
delay_ms: exports_external.coerce.number().optional().describe("Wait time (ms) after paste before Enter. Default: adaptive 25-1500ms"),
|
|
44199
|
+
stagger_ms: exports_external.coerce.number().optional().describe("Delay (ms) between sending to each target (default: 500)"),
|
|
44200
|
+
retries: exports_external.coerce.number().optional().describe("Max retry attempts per target (default: 3)"),
|
|
44201
|
+
verify: exports_external.coerce.boolean().optional().describe("Verify each message was submitted (default: true)")
|
|
44202
|
+
}
|
|
44203
|
+
}, async (args) => {
|
|
44204
|
+
const { targets, message, delay_ms, stagger_ms, retries, verify } = args;
|
|
44205
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
44206
|
+
return { content: [{ type: "text", text: "targets must be a non-empty array" }], isError: true };
|
|
44207
|
+
}
|
|
44208
|
+
if (!message || !message.trim()) {
|
|
44209
|
+
return { content: [{ type: "text", text: "message cannot be empty" }], isError: true };
|
|
44210
|
+
}
|
|
44211
|
+
const stagger = typeof stagger_ms === "number" && stagger_ms >= 0 ? stagger_ms : 500;
|
|
44212
|
+
const results = new Array(targets.length);
|
|
44213
|
+
await Promise.all(targets.map(async (rawTarget, i) => {
|
|
44214
|
+
const target = String(rawTarget).trim();
|
|
44215
|
+
if (i > 0 && stagger > 0)
|
|
44216
|
+
await sleep2(stagger * i);
|
|
44217
|
+
try {
|
|
44218
|
+
const result = await tmuxSend(target, message, {
|
|
44219
|
+
delayMs: typeof delay_ms === "number" && delay_ms > 0 ? delay_ms : undefined,
|
|
44220
|
+
retries: typeof retries === "number" && retries > 0 ? retries : undefined,
|
|
44221
|
+
verify: verify !== false
|
|
44222
|
+
});
|
|
44223
|
+
results[i] = { target, ...result };
|
|
44224
|
+
} catch (err) {
|
|
44225
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
44226
|
+
results[i] = { target, success: false, attempts: 0, error: errMsg };
|
|
44227
|
+
}
|
|
44228
|
+
}));
|
|
44229
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
44230
|
+
const failed = results.length - succeeded;
|
|
44231
|
+
return {
|
|
44232
|
+
content: [{
|
|
44233
|
+
type: "text",
|
|
44234
|
+
text: JSON.stringify({ results, succeeded, failed, total: results.length })
|
|
44235
|
+
}],
|
|
44236
|
+
isError: failed > 0
|
|
44237
|
+
};
|
|
44238
|
+
});
|
|
44239
|
+
}
|
|
44052
44240
|
// package.json
|
|
44053
44241
|
var package_default = {
|
|
44054
44242
|
name: "@hasna/conversations",
|
|
44055
|
-
version: "0.2.
|
|
44243
|
+
version: "0.2.31",
|
|
44056
44244
|
description: "Real-time CLI messaging for AI agents",
|
|
44057
44245
|
type: "module",
|
|
44058
44246
|
bin: {
|
|
@@ -44154,6 +44342,8 @@ registerSpaceTools(server);
|
|
|
44154
44342
|
registerProjectTools(server);
|
|
44155
44343
|
registerAgentTools(server, agentFocus, getAgentFocus);
|
|
44156
44344
|
registerAdvancedTools(server, package_default.version);
|
|
44345
|
+
registerTmuxTools(server);
|
|
44346
|
+
registerChannelBridge(server, () => process.env.CONVERSATIONS_AGENT_ID ?? null);
|
|
44157
44347
|
async function startMcpServer() {
|
|
44158
44348
|
const transport = new StdioServerTransport;
|
|
44159
44349
|
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.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Real-time CLI messaging for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -77,4 +77,4 @@
|
|
|
77
77
|
"url": "https://github.com/hasna/conversations/issues"
|
|
78
78
|
},
|
|
79
79
|
"homepage": "https://github.com/hasna/conversations#readme"
|
|
80
|
-
}
|
|
80
|
+
}
|