@hasna/conversations 0.2.28 → 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 +301 -18
- package/bin/mcp.js +175 -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 +1 -1
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.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
|
|
47679
|
+
import chalk10 from "chalk";
|
|
47407
47680
|
import { render } from "ink";
|
|
47408
47681
|
import React8 from "react";
|
|
47409
47682
|
|
|
@@ -48518,17 +48791,18 @@ init_presence();
|
|
|
48518
48791
|
init_db();
|
|
48519
48792
|
import chalk4 from "chalk";
|
|
48520
48793
|
function registerMessagingCommands(program2) {
|
|
48521
|
-
program2.command("send").description("Send a message to an agent").argument("<message>", "Message content").
|
|
48794
|
+
program2.command("send").description("Send a message to an agent").argument("<message>", "Message content").option("--to <agent>", "Recipient agent ID (required unless --space is used)").option("--from <agent>", "Sender agent ID").option("--session <id>", "Session ID (auto-generated if omitted)").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--working-dir <path>", "Working directory context").option("--repository <repo>", "Repository context").option("--branch <branch>", "Branch context").option("--metadata <json>", "JSON metadata string").option("--space <name>", "Send to a space instead of a specific agent").option("--blocking", "Send as a blocking message (recipient must acknowledge)").option("--json", "Output as JSON").action((message, opts) => {
|
|
48522
48795
|
const from = resolveIdentity(opts.from).trim();
|
|
48523
48796
|
const to = typeof opts.to === "string" ? opts.to.trim() : "";
|
|
48797
|
+
const space = typeof opts.space === "string" ? opts.space.trim() : "";
|
|
48524
48798
|
const content = typeof message === "string" ? message : "";
|
|
48525
48799
|
const session = typeof opts.session === "string" && opts.session.trim() ? opts.session.trim() : undefined;
|
|
48526
48800
|
if (!from) {
|
|
48527
48801
|
console.error(chalk4.red("Sender identity is required."));
|
|
48528
48802
|
process.exit(1);
|
|
48529
48803
|
}
|
|
48530
|
-
if (!to) {
|
|
48531
|
-
console.error(chalk4.red("Recipient is required
|
|
48804
|
+
if (!to && !space) {
|
|
48805
|
+
console.error(chalk4.red("Recipient is required: use --to <agent> or --space <name>."));
|
|
48532
48806
|
process.exit(1);
|
|
48533
48807
|
}
|
|
48534
48808
|
if (!content.trim()) {
|
|
@@ -48546,7 +48820,8 @@ function registerMessagingCommands(program2) {
|
|
|
48546
48820
|
}
|
|
48547
48821
|
const msg = sendMessage({
|
|
48548
48822
|
from,
|
|
48549
|
-
to,
|
|
48823
|
+
to: to || from,
|
|
48824
|
+
space: space || undefined,
|
|
48550
48825
|
content,
|
|
48551
48826
|
session_id: session,
|
|
48552
48827
|
priority: opts.priority,
|
|
@@ -48558,6 +48833,8 @@ function registerMessagingCommands(program2) {
|
|
|
48558
48833
|
});
|
|
48559
48834
|
if (opts.json) {
|
|
48560
48835
|
console.log(JSON.stringify(msg, null, 2));
|
|
48836
|
+
} else if (space) {
|
|
48837
|
+
console.log(chalk4.green(`Message sent to #${space}`) + chalk4.dim(` (id: ${msg.id})`));
|
|
48561
48838
|
} else {
|
|
48562
48839
|
console.log(chalk4.green(`Message sent`) + chalk4.dim(` (id: ${msg.id}, session: ${msg.session_id})`));
|
|
48563
48840
|
}
|
|
@@ -49412,7 +49689,7 @@ function registerProjectCommands(program2) {
|
|
|
49412
49689
|
}
|
|
49413
49690
|
closeDb();
|
|
49414
49691
|
});
|
|
49415
|
-
project.command("update").description("Update a project").argument("<id>", "Project ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--path <path>", "New path").option("--status <status>", "New status (active/archived)").option("--repository <url>", "New repository URL").option("--tags <json>", "New tags (JSON array)").option("--json", "Output as JSON").action((id, opts) => {
|
|
49692
|
+
project.command("update").description("Update a project").argument("<id-or-name>", "Project ID or name").option("--name <name>", "New name").option("--description <text>", "New description").option("--path <path>", "New path").option("--status <status>", "New status (active/archived)").option("--repository <url>", "New repository URL").option("--tags <json>", "New tags (JSON array)").option("--json", "Output as JSON").action((id, opts) => {
|
|
49416
49693
|
const updates = {};
|
|
49417
49694
|
if (opts.name)
|
|
49418
49695
|
updates.name = opts.name;
|
|
@@ -49433,7 +49710,9 @@ function registerProjectCommands(program2) {
|
|
|
49433
49710
|
}
|
|
49434
49711
|
}
|
|
49435
49712
|
try {
|
|
49436
|
-
const
|
|
49713
|
+
const isUuid = /^[0-9a-f-]{36}$/i.test(id);
|
|
49714
|
+
const resolvedId = isUuid ? id : getProjectByName(id)?.id ?? id;
|
|
49715
|
+
const p = updateProject(resolvedId, updates);
|
|
49437
49716
|
if (opts.json) {
|
|
49438
49717
|
console.log(JSON.stringify(p, null, 2));
|
|
49439
49718
|
} else {
|
|
@@ -49445,9 +49724,11 @@ function registerProjectCommands(program2) {
|
|
|
49445
49724
|
}
|
|
49446
49725
|
closeDb();
|
|
49447
49726
|
});
|
|
49448
|
-
project.command("delete").description("Delete a project").argument("<id>", "Project ID").option("--json", "Output as JSON").action((id, opts) => {
|
|
49727
|
+
project.command("delete").description("Delete a project").argument("<id-or-name>", "Project ID or name").option("--json", "Output as JSON").action((id, opts) => {
|
|
49449
49728
|
try {
|
|
49450
|
-
const
|
|
49729
|
+
const isUuid = /^[0-9a-f-]{36}$/i.test(id);
|
|
49730
|
+
const resolvedId = isUuid ? id : getProjectByName(id)?.id ?? id;
|
|
49731
|
+
const deleted = deleteProject(resolvedId);
|
|
49451
49732
|
if (!deleted) {
|
|
49452
49733
|
console.error(chalk6.red(`Project "${id}" not found.`));
|
|
49453
49734
|
process.exit(1);
|
|
@@ -50008,6 +50289,7 @@ function registerAnalyticsCommands(program2) {
|
|
|
50008
50289
|
}
|
|
50009
50290
|
|
|
50010
50291
|
// src/cli/index.tsx
|
|
50292
|
+
init_tmux();
|
|
50011
50293
|
var import__package3 = __toESM(require_package(), 1);
|
|
50012
50294
|
var program2 = new Command;
|
|
50013
50295
|
program2.name("conversations").description("Real-time CLI messaging for AI agents").version(import__package3.default.version);
|
|
@@ -50016,6 +50298,7 @@ registerSpaceCommands(program2);
|
|
|
50016
50298
|
registerProjectCommands(program2);
|
|
50017
50299
|
registerAgentCommands(program2);
|
|
50018
50300
|
registerAnalyticsCommands(program2);
|
|
50301
|
+
registerTmuxCommands(program2);
|
|
50019
50302
|
program2.command("mcp").description("Start MCP server").action(async () => {
|
|
50020
50303
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
50021
50304
|
await startMcpServer2();
|
|
@@ -50040,11 +50323,11 @@ registerCloudCommands(program2, "conversations");
|
|
|
50040
50323
|
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
50041
50324
|
const config2 = getCloudConfig2();
|
|
50042
50325
|
if (config2.mode === "local") {
|
|
50043
|
-
console.error(
|
|
50326
|
+
console.error(chalk10.red("Error: cloud mode not configured. Set RDS credentials first."));
|
|
50044
50327
|
process.exit(1);
|
|
50045
50328
|
}
|
|
50046
50329
|
if (opts.dryRun) {
|
|
50047
|
-
console.log(
|
|
50330
|
+
console.log(chalk10.dim(`-- Dry run: SQL that would be executed --
|
|
50048
50331
|
`));
|
|
50049
50332
|
for (const sql of PG_MIGRATIONS2)
|
|
50050
50333
|
console.log(sql);
|
|
@@ -50052,14 +50335,14 @@ registerCloudCommands(program2, "conversations");
|
|
|
50052
50335
|
}
|
|
50053
50336
|
const pg = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
50054
50337
|
for (let i = 0;i < PG_MIGRATIONS2.length; i++) {
|
|
50055
|
-
process.stdout.write(
|
|
50338
|
+
process.stdout.write(chalk10.dim(`Running migration ${i + 1}/${PG_MIGRATIONS2.length}...`));
|
|
50056
50339
|
await pg.run(PG_MIGRATIONS2[i]);
|
|
50057
|
-
console.log(
|
|
50340
|
+
console.log(chalk10.green(" done"));
|
|
50058
50341
|
}
|
|
50059
50342
|
await pg.close();
|
|
50060
|
-
console.log(
|
|
50343
|
+
console.log(chalk10.green("\u2713 All migrations applied."));
|
|
50061
50344
|
} catch (e) {
|
|
50062
|
-
console.error(
|
|
50345
|
+
console.error(chalk10.red(`Migration failed: ${e?.message ?? e}`));
|
|
50063
50346
|
process.exit(1);
|
|
50064
50347
|
}
|
|
50065
50348
|
});
|
|
@@ -50067,8 +50350,8 @@ registerCloudCommands(program2, "conversations");
|
|
|
50067
50350
|
}
|
|
50068
50351
|
program2.action(() => {
|
|
50069
50352
|
if (!process.stdin.isTTY) {
|
|
50070
|
-
console.error(
|
|
50071
|
-
console.error(
|
|
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."));
|
|
50072
50355
|
process.exit(1);
|
|
50073
50356
|
}
|
|
50074
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.
|
|
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;
|