@hasna/conversations 0.1.16 → 0.1.18

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/hook.js CHANGED
@@ -120,6 +120,17 @@ function getDb() {
120
120
  metadata TEXT
121
121
  )
122
122
  `);
123
+ db.exec(`
124
+ CREATE TABLE IF NOT EXISTS reactions (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
127
+ agent TEXT NOT NULL,
128
+ emoji TEXT NOT NULL,
129
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
130
+ UNIQUE(message_id, agent, emoji)
131
+ )
132
+ `);
133
+ db.exec("CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id)");
123
134
  const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
124
135
  const tableNames = existingTables.map((t) => t.name);
125
136
  if (tableNames.includes("channels") && tableNames.includes("spaces")) {
@@ -177,6 +188,9 @@ function getDb() {
177
188
  db.exec("ALTER TABLE messages ADD COLUMN blocking INTEGER NOT NULL DEFAULT 0");
178
189
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_blocking ON messages(blocking)");
179
190
  }
191
+ if (!colNames2.includes("attachments")) {
192
+ db.exec("ALTER TABLE messages ADD COLUMN attachments TEXT");
193
+ }
180
194
  return db;
181
195
  }
182
196
  function closeDb() {
package/bin/index.js CHANGED
@@ -1974,6 +1974,17 @@ function getDb() {
1974
1974
  metadata TEXT
1975
1975
  )
1976
1976
  `);
1977
+ db.exec(`
1978
+ CREATE TABLE IF NOT EXISTS reactions (
1979
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1980
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
1981
+ agent TEXT NOT NULL,
1982
+ emoji TEXT NOT NULL,
1983
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1984
+ UNIQUE(message_id, agent, emoji)
1985
+ )
1986
+ `);
1987
+ db.exec("CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id)");
1977
1988
  const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
1978
1989
  const tableNames = existingTables.map((t) => t.name);
1979
1990
  if (tableNames.includes("channels") && tableNames.includes("spaces")) {
@@ -2031,6 +2042,9 @@ function getDb() {
2031
2042
  db.exec("ALTER TABLE messages ADD COLUMN blocking INTEGER NOT NULL DEFAULT 0");
2032
2043
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_blocking ON messages(blocking)");
2033
2044
  }
2045
+ if (!colNames2.includes("attachments")) {
2046
+ db.exec("ALTER TABLE messages ADD COLUMN attachments TEXT");
2047
+ }
2034
2048
  return db;
2035
2049
  }
2036
2050
  function closeDb() {
@@ -2044,6 +2058,9 @@ var init_db = () => {};
2044
2058
 
2045
2059
  // src/lib/messages.ts
2046
2060
  import { randomUUID } from "crypto";
2061
+ import { mkdirSync as mkdirSync2, copyFileSync, statSync } from "fs";
2062
+ import { join as join2 } from "path";
2063
+ import { homedir as homedir2 } from "os";
2047
2064
  function parseMessage(row) {
2048
2065
  let metadata = null;
2049
2066
  if (row.metadata) {
@@ -2053,12 +2070,53 @@ function parseMessage(row) {
2053
2070
  metadata = null;
2054
2071
  }
2055
2072
  }
2073
+ let attachments = null;
2074
+ if (row.attachments) {
2075
+ try {
2076
+ attachments = JSON.parse(row.attachments);
2077
+ } catch {
2078
+ attachments = null;
2079
+ }
2080
+ }
2056
2081
  return {
2057
2082
  ...row,
2058
2083
  metadata,
2084
+ attachments,
2059
2085
  blocking: !!row.blocking
2060
2086
  };
2061
2087
  }
2088
+ function getAttachmentsDir() {
2089
+ if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
2090
+ return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
2091
+ return join2(homedir2(), ".conversations", "attachments");
2092
+ }
2093
+ function guessMimeType(name) {
2094
+ const ext = name.split(".").pop()?.toLowerCase();
2095
+ const mimeMap = {
2096
+ txt: "text/plain",
2097
+ md: "text/markdown",
2098
+ json: "application/json",
2099
+ js: "text/javascript",
2100
+ ts: "text/typescript",
2101
+ py: "text/x-python",
2102
+ html: "text/html",
2103
+ css: "text/css",
2104
+ xml: "application/xml",
2105
+ png: "image/png",
2106
+ jpg: "image/jpeg",
2107
+ jpeg: "image/jpeg",
2108
+ gif: "image/gif",
2109
+ svg: "image/svg+xml",
2110
+ webp: "image/webp",
2111
+ pdf: "application/pdf",
2112
+ zip: "application/zip",
2113
+ gz: "application/gzip",
2114
+ csv: "text/csv",
2115
+ yaml: "text/yaml",
2116
+ yml: "text/yaml"
2117
+ };
2118
+ return mimeMap[ext || ""] || "application/octet-stream";
2119
+ }
2062
2120
  function sendMessage(opts) {
2063
2121
  const db2 = getDb();
2064
2122
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
@@ -2072,7 +2130,27 @@ function sendMessage(opts) {
2072
2130
  RETURNING *
2073
2131
  `);
2074
2132
  const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking);
2075
- return parseMessage(row);
2133
+ const message = parseMessage(row);
2134
+ if (opts.attachments && opts.attachments.length > 0) {
2135
+ const attachmentsDir = join2(getAttachmentsDir(), String(message.id));
2136
+ mkdirSync2(attachmentsDir, { recursive: true });
2137
+ const attachmentInfos = [];
2138
+ for (const att of opts.attachments) {
2139
+ const destPath = join2(attachmentsDir, att.name);
2140
+ copyFileSync(att.source_path, destPath);
2141
+ const stat = statSync(destPath);
2142
+ attachmentInfos.push({
2143
+ name: att.name,
2144
+ path: destPath,
2145
+ size: stat.size,
2146
+ mime_type: guessMimeType(att.name)
2147
+ });
2148
+ }
2149
+ const attachmentsJson = JSON.stringify(attachmentInfos);
2150
+ db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
2151
+ message.attachments = attachmentInfos;
2152
+ }
2153
+ return message;
2076
2154
  }
2077
2155
  function readMessages(opts = {}) {
2078
2156
  const db2 = getDb();
@@ -3012,9 +3090,9 @@ var init_names = __esm(() => {
3012
3090
  });
3013
3091
 
3014
3092
  // src/lib/identity.ts
3015
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3016
- import { join as join2, dirname as dirname2 } from "path";
3017
- import { homedir as homedir2 } from "os";
3093
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
3094
+ import { join as join3, dirname as dirname2 } from "path";
3095
+ import { homedir as homedir3 } from "os";
3018
3096
  function isNameTaken(name) {
3019
3097
  try {
3020
3098
  const { getDb: getDb2 } = (init_db(), __toCommonJS(exports_db));
@@ -3045,7 +3123,7 @@ function getAutoName() {
3045
3123
  }
3046
3124
  cachedAutoName = name;
3047
3125
  try {
3048
- mkdirSync2(dirname2(AGENT_ID_FILE), { recursive: true });
3126
+ mkdirSync3(dirname2(AGENT_ID_FILE), { recursive: true });
3049
3127
  writeFileSync(AGENT_ID_FILE, name + `
3050
3128
  `, "utf-8");
3051
3129
  } catch {}
@@ -3063,7 +3141,7 @@ function resolveIdentity(explicit) {
3063
3141
  var AGENT_ID_FILE, cachedAutoName = null;
3064
3142
  var init_identity = __esm(() => {
3065
3143
  init_names();
3066
- AGENT_ID_FILE = join2(homedir2(), ".conversations", "agent-id");
3144
+ AGENT_ID_FILE = join3(homedir3(), ".conversations", "agent-id");
3067
3145
  });
3068
3146
 
3069
3147
  // src/lib/presence.ts
@@ -3101,6 +3179,11 @@ function heartbeat(agent, status, metadata) {
3101
3179
  metadata = excluded.metadata
3102
3180
  `).run(agent, resolvedStatus, metadataJson);
3103
3181
  }
3182
+ function getPresence(agent) {
3183
+ const db2 = getDb();
3184
+ const row = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(agent);
3185
+ return row ? parsePresence(row) : null;
3186
+ }
3104
3187
  function listAgents(opts) {
3105
3188
  const db2 = getDb();
3106
3189
  let query = "SELECT * FROM agent_presence";
@@ -3231,7 +3314,7 @@ var init_poll = __esm(() => {
3231
3314
  var require_package = __commonJS((exports, module) => {
3232
3315
  module.exports = {
3233
3316
  name: "@hasna/conversations",
3234
- version: "0.1.16",
3317
+ version: "0.1.18",
3235
3318
  description: "Real-time CLI messaging for AI agents",
3236
3319
  type: "module",
3237
3320
  bin: {
@@ -32966,7 +33049,7 @@ var exports_serve = {};
32966
33049
  __export(exports_serve, {
32967
33050
  startDashboardServer: () => startDashboardServer
32968
33051
  });
32969
- import { join as join3, resolve, sep } from "path";
33052
+ import { join as join4, resolve, sep } from "path";
32970
33053
  import { existsSync } from "fs";
32971
33054
  function securityHeaders(base) {
32972
33055
  const headers = new Headers(base);
@@ -33037,7 +33120,7 @@ function isSameOrigin(req) {
33037
33120
  function startDashboardServer(port = 0, host) {
33038
33121
  const resolvedPort = normalizePort(port, 0);
33039
33122
  const resolvedHost = normalizeHost(host ?? process.env.CONVERSATIONS_DASHBOARD_HOST);
33040
- const dashboardDist = join3(import.meta.dir, "../../dashboard/dist");
33123
+ const dashboardDist = join4(import.meta.dir, "../../dashboard/dist");
33041
33124
  const hasDist = existsSync(dashboardDist);
33042
33125
  const server2 = Bun.serve({
33043
33126
  port: resolvedPort,
@@ -33421,7 +33504,7 @@ function startDashboardServer(port = 0, host) {
33421
33504
  headers.set("Content-Type", file2.type);
33422
33505
  return new Response(file2, { headers });
33423
33506
  }
33424
- file2 = Bun.file(join3(dashboardDist, "index.html"));
33507
+ file2 = Bun.file(join4(dashboardDist, "index.html"));
33425
33508
  if (await file2.exists()) {
33426
33509
  const headers = securityHeaders();
33427
33510
  if (file2.type)
@@ -35235,6 +35318,38 @@ agents.command("rename").description("Rename an agent in the presence list").arg
35235
35318
  }
35236
35319
  closeDb();
35237
35320
  });
35321
+ program2.command("whoami").description("Show current agent identity and online status").option("--from <agent>", "Explicit agent identity").action((opts) => {
35322
+ const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
35323
+ const agent = resolveIdentity(opts.from);
35324
+ let source;
35325
+ if (opts.from) {
35326
+ source = "explicit (--from flag)";
35327
+ } else if (envValue) {
35328
+ source = "env var (CONVERSATIONS_AGENT_ID)";
35329
+ } else {
35330
+ const { join: join5 } = __require("path");
35331
+ const { homedir: homedir4 } = __require("os");
35332
+ const agentIdFile = join5(homedir4(), ".conversations", "agent-id");
35333
+ source = `auto-generated (${agentIdFile})`;
35334
+ }
35335
+ const presence = getPresence(agent);
35336
+ let onlineStatus;
35337
+ if (presence && presence.online) {
35338
+ const lastSeenMs = new Date(presence.last_seen_at + "Z").getTime();
35339
+ const agoMs = Date.now() - lastSeenMs;
35340
+ const agoSec = Math.floor(agoMs / 1000);
35341
+ const agoStr = agoSec < 60 ? `${agoSec}s ago` : `${Math.floor(agoSec / 60)}m ago`;
35342
+ onlineStatus = chalk2.green(`yes`) + chalk2.dim(` (last seen ${agoStr})`);
35343
+ } else if (presence) {
35344
+ onlineStatus = chalk2.red("no") + chalk2.dim(` (last seen ${presence.last_seen_at})`);
35345
+ } else {
35346
+ onlineStatus = chalk2.red("no") + chalk2.dim(" (no presence record)");
35347
+ }
35348
+ console.log(` ${chalk2.bold("Agent:")} ${chalk2.cyan(agent)}`);
35349
+ console.log(` ${chalk2.bold("Source:")} ${source}`);
35350
+ console.log(` ${chalk2.bold("Online:")} ${onlineStatus}`);
35351
+ closeDb();
35352
+ });
35238
35353
  program2.command("blockers").description("Check for unread blocking messages").option("--from <agent>", "Agent to check blockers for").option("--json", "Output as JSON").action((opts) => {
35239
35354
  const agent = resolveIdentity(opts.from);
35240
35355
  const blockers = getUnreadBlockers(agent);
@@ -35257,14 +35372,21 @@ Acknowledge with: conversations mark-read ${blockers.map((b) => b.id).join(" ")}
35257
35372
  }
35258
35373
  closeDb();
35259
35374
  });
35260
- program2.command("watch").description("Watch for new messages with desktop notifications").option("--from <agent>", "Your agent identity").option("--space <name>", "Watch a specific space").option("--interval <ms>", "Poll interval in milliseconds", parseInt).action((opts) => {
35375
+ program2.command("watch").description("Watch for new messages with desktop notifications").option("--from <agent>", "Your agent identity").option("--space <name>", "Watch a specific space").option("--all", "Watch DMs and all subscribed spaces").option("--interval <ms>", "Poll interval in milliseconds", parseInt).action((opts) => {
35261
35376
  const agent = resolveIdentity(opts.from);
35262
35377
  heartbeat(agent);
35263
35378
  const interval = Number.isFinite(opts.interval) && opts.interval > 0 ? opts.interval : 1000;
35264
35379
  const cols = Math.min(process.stdout.columns || 80, 100);
35380
+ let agentSpaces = [];
35381
+ if (opts.all) {
35382
+ const db2 = getDb();
35383
+ const rows = db2.prepare("SELECT space FROM space_members WHERE agent = ?").all(agent);
35384
+ agentSpaces = rows.map((r) => r.space);
35385
+ }
35386
+ const modeLabel = opts.all ? `DMs + ${agentSpaces.length} space(s)` : opts.space ? `Space: #${opts.space}` : "All DMs";
35265
35387
  console.log("");
35266
35388
  console.log(chalk2.bold(` Conversations`) + chalk2.dim(` \u2014 watching as ${chalk2.cyan(agent)}`));
35267
- console.log(chalk2.dim(` ${opts.space ? `Space: #${opts.space}` : "All DMs"} \xB7 Poll: ${interval}ms \xB7 Ctrl+C to stop`));
35389
+ console.log(chalk2.dim(` ${modeLabel} \xB7 Poll: ${interval}ms \xB7 Ctrl+C to stop`));
35268
35390
  console.log(chalk2.dim(" " + "\u2500".repeat(cols - 4)));
35269
35391
  console.log("");
35270
35392
  const { startPolling: startPolling2 } = (init_poll(), __toCommonJS(exports_poll));
@@ -35310,8 +35432,8 @@ program2.command("watch").description("Watch for new messages with desktop notif
35310
35432
  if (process.platform === "darwin") {
35311
35433
  try {
35312
35434
  const { execSync } = __require("child_process");
35313
- const t = title.replace(/"/g, "\\\"");
35314
- const b = body.replace(/"/g, "\\\"").replace(/\n/g, " ").slice(0, 200);
35435
+ const t = title.replace(/['"\\]/g, " ");
35436
+ const b = body.replace(/['"\\]/g, " ").replace(/\n/g, " ").slice(0, 200);
35315
35437
  execSync(`osascript -e 'display notification "${b}" with title "${t}"'`, { timeout: 3000 });
35316
35438
  } catch {}
35317
35439
  }
@@ -35331,21 +35453,62 @@ program2.command("watch").description("Watch for new messages with desktop notif
35331
35453
  console.log(chalk2.dim(" " + "\xB7".repeat(Math.min(cols - 8, 60))));
35332
35454
  console.log("");
35333
35455
  };
35334
- startPolling2({
35335
- to_agent: opts.space ? undefined : agent,
35336
- space: opts.space,
35337
- interval_ms: interval,
35338
- on_messages: (messages) => {
35339
- for (const msg of messages) {
35340
- if (msg.from_agent === agent)
35341
- continue;
35456
+ if (opts.all) {
35457
+ const dmRecent = readMessages({ to: agent, limit: 20, order: "asc" });
35458
+ const spaceRecent = [];
35459
+ for (const sp of agentSpaces) {
35460
+ spaceRecent.push(...readMessages({ space: sp, limit: 10, order: "asc" }));
35461
+ }
35462
+ const recent = [...dmRecent, ...spaceRecent].sort((a, b) => a.created_at.localeCompare(b.created_at)).slice(-20);
35463
+ if (recent.length > 0) {
35464
+ console.log(chalk2.dim(` \u2500\u2500 Recent messages (${recent.length}) \u2500\u2500
35465
+ `));
35466
+ for (const msg of recent) {
35342
35467
  renderMessage(msg);
35343
- const where = msg.space ? `#${msg.space}` : "DM";
35344
- const preview = msg.content.replace(/[*#`~_>\-]/g, "").slice(0, 150);
35345
- desktopNotify(`${msg.from_agent} (${where})`, preview);
35346
35468
  }
35469
+ console.log(chalk2.dim(` \u2500\u2500 Live \u2500\u2500
35470
+ `));
35347
35471
  }
35348
- });
35472
+ } else {
35473
+ const recent = readMessages({
35474
+ to: opts.space ? undefined : agent,
35475
+ space: opts.space,
35476
+ limit: 20,
35477
+ order: "asc"
35478
+ });
35479
+ if (recent.length > 0) {
35480
+ console.log(chalk2.dim(` \u2500\u2500 Recent messages (${recent.length}) \u2500\u2500
35481
+ `));
35482
+ for (const msg of recent) {
35483
+ renderMessage(msg);
35484
+ }
35485
+ console.log(chalk2.dim(` \u2500\u2500 Live \u2500\u2500
35486
+ `));
35487
+ }
35488
+ }
35489
+ const onNewMessages = (messages) => {
35490
+ for (const msg of messages) {
35491
+ if (msg.from_agent === agent)
35492
+ continue;
35493
+ renderMessage(msg);
35494
+ const where = msg.space ? `#${msg.space}` : "DM";
35495
+ const preview = msg.content.replace(/[*#`~_>\-]/g, "").slice(0, 150);
35496
+ desktopNotify(`${msg.from_agent} (${where})`, preview);
35497
+ }
35498
+ };
35499
+ if (opts.all) {
35500
+ startPolling2({ to_agent: agent, interval_ms: interval, on_messages: onNewMessages });
35501
+ for (const sp of agentSpaces) {
35502
+ startPolling2({ space: sp, interval_ms: interval, on_messages: onNewMessages });
35503
+ }
35504
+ } else {
35505
+ startPolling2({
35506
+ to_agent: opts.space ? undefined : agent,
35507
+ space: opts.space,
35508
+ interval_ms: interval,
35509
+ on_messages: onNewMessages
35510
+ });
35511
+ }
35349
35512
  process.on("SIGINT", () => {
35350
35513
  console.log(chalk2.dim(`
35351
35514
  Stopped watching.`));
@@ -35357,10 +35520,14 @@ program2.command("mcp").description("Start MCP server").action(async () => {
35357
35520
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
35358
35521
  await startMcpServer2();
35359
35522
  });
35360
- program2.command("dashboard").description("Start web dashboard").option("--port <port>", "Port to listen on", parseInt).option("--host <host>", "Host to bind (default: 127.0.0.1)").action(async (opts) => {
35523
+ program2.command("dashboard").description("Start web dashboard").option("--port <port>", "Port to listen on", parseInt).option("--host <host>", "Host to bind (default: 127.0.0.1)").option("--open", "Auto-open dashboard in browser").action(async (opts) => {
35361
35524
  const { startDashboardServer: startDashboardServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
35362
35525
  const port = Number.isFinite(opts.port) && opts.port >= 0 && opts.port <= 65535 ? opts.port : 0;
35363
- startDashboardServer2(port, opts.host);
35526
+ const server2 = startDashboardServer2(port, opts.host);
35527
+ if (opts.open) {
35528
+ const { exec } = __require("child_process");
35529
+ exec(`open http://localhost:${server2.port}`);
35530
+ }
35364
35531
  });
35365
35532
  program2.action(() => {
35366
35533
  if (!process.stdin.isTTY) {
package/bin/mcp.js CHANGED
@@ -6606,6 +6606,17 @@ function getDb() {
6606
6606
  metadata TEXT
6607
6607
  )
6608
6608
  `);
6609
+ db.exec(`
6610
+ CREATE TABLE IF NOT EXISTS reactions (
6611
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6612
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
6613
+ agent TEXT NOT NULL,
6614
+ emoji TEXT NOT NULL,
6615
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6616
+ UNIQUE(message_id, agent, emoji)
6617
+ )
6618
+ `);
6619
+ db.exec("CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id)");
6609
6620
  const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
6610
6621
  const tableNames = existingTables.map((t) => t.name);
6611
6622
  if (tableNames.includes("channels") && tableNames.includes("spaces")) {
@@ -6663,6 +6674,9 @@ function getDb() {
6663
6674
  db.exec("ALTER TABLE messages ADD COLUMN blocking INTEGER NOT NULL DEFAULT 0");
6664
6675
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_blocking ON messages(blocking)");
6665
6676
  }
6677
+ if (!colNames2.includes("attachments")) {
6678
+ db.exec("ALTER TABLE messages ADD COLUMN attachments TEXT");
6679
+ }
6666
6680
  return db;
6667
6681
  }
6668
6682
  function closeDb() {
@@ -28494,6 +28508,9 @@ class StdioServerTransport {
28494
28508
  // src/lib/messages.ts
28495
28509
  init_db();
28496
28510
  import { randomUUID } from "crypto";
28511
+ import { mkdirSync as mkdirSync2, copyFileSync, statSync } from "fs";
28512
+ import { join as join2 } from "path";
28513
+ import { homedir as homedir2 } from "os";
28497
28514
  function parseMessage(row) {
28498
28515
  let metadata = null;
28499
28516
  if (row.metadata) {
@@ -28503,12 +28520,53 @@ function parseMessage(row) {
28503
28520
  metadata = null;
28504
28521
  }
28505
28522
  }
28523
+ let attachments = null;
28524
+ if (row.attachments) {
28525
+ try {
28526
+ attachments = JSON.parse(row.attachments);
28527
+ } catch {
28528
+ attachments = null;
28529
+ }
28530
+ }
28506
28531
  return {
28507
28532
  ...row,
28508
28533
  metadata,
28534
+ attachments,
28509
28535
  blocking: !!row.blocking
28510
28536
  };
28511
28537
  }
28538
+ function getAttachmentsDir() {
28539
+ if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
28540
+ return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
28541
+ return join2(homedir2(), ".conversations", "attachments");
28542
+ }
28543
+ function guessMimeType(name) {
28544
+ const ext = name.split(".").pop()?.toLowerCase();
28545
+ const mimeMap = {
28546
+ txt: "text/plain",
28547
+ md: "text/markdown",
28548
+ json: "application/json",
28549
+ js: "text/javascript",
28550
+ ts: "text/typescript",
28551
+ py: "text/x-python",
28552
+ html: "text/html",
28553
+ css: "text/css",
28554
+ xml: "application/xml",
28555
+ png: "image/png",
28556
+ jpg: "image/jpeg",
28557
+ jpeg: "image/jpeg",
28558
+ gif: "image/gif",
28559
+ svg: "image/svg+xml",
28560
+ webp: "image/webp",
28561
+ pdf: "application/pdf",
28562
+ zip: "application/zip",
28563
+ gz: "application/gzip",
28564
+ csv: "text/csv",
28565
+ yaml: "text/yaml",
28566
+ yml: "text/yaml"
28567
+ };
28568
+ return mimeMap[ext || ""] || "application/octet-stream";
28569
+ }
28512
28570
  function sendMessage(opts) {
28513
28571
  const db2 = getDb();
28514
28572
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
@@ -28522,7 +28580,27 @@ function sendMessage(opts) {
28522
28580
  RETURNING *
28523
28581
  `);
28524
28582
  const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking);
28525
- return parseMessage(row);
28583
+ const message = parseMessage(row);
28584
+ if (opts.attachments && opts.attachments.length > 0) {
28585
+ const attachmentsDir = join2(getAttachmentsDir(), String(message.id));
28586
+ mkdirSync2(attachmentsDir, { recursive: true });
28587
+ const attachmentInfos = [];
28588
+ for (const att of opts.attachments) {
28589
+ const destPath = join2(attachmentsDir, att.name);
28590
+ copyFileSync(att.source_path, destPath);
28591
+ const stat = statSync(destPath);
28592
+ attachmentInfos.push({
28593
+ name: att.name,
28594
+ path: destPath,
28595
+ size: stat.size,
28596
+ mime_type: guessMimeType(att.name)
28597
+ });
28598
+ }
28599
+ const attachmentsJson = JSON.stringify(attachmentInfos);
28600
+ db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
28601
+ message.attachments = attachmentInfos;
28602
+ }
28603
+ return message;
28526
28604
  }
28527
28605
  function readMessages(opts = {}) {
28528
28606
  const db2 = getDb();
@@ -29085,9 +29163,9 @@ function deleteProject(id) {
29085
29163
  }
29086
29164
 
29087
29165
  // src/lib/identity.ts
29088
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
29089
- import { join as join2, dirname as dirname2 } from "path";
29090
- import { homedir as homedir2 } from "os";
29166
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
29167
+ import { join as join3, dirname as dirname2 } from "path";
29168
+ import { homedir as homedir3 } from "os";
29091
29169
 
29092
29170
  // src/lib/names.ts
29093
29171
  var AGENT_NAMES = [
@@ -29439,7 +29517,7 @@ var AGENT_NAMES = [
29439
29517
  ];
29440
29518
 
29441
29519
  // src/lib/identity.ts
29442
- var AGENT_ID_FILE = join2(homedir2(), ".conversations", "agent-id");
29520
+ var AGENT_ID_FILE = join3(homedir3(), ".conversations", "agent-id");
29443
29521
  var cachedAutoName = null;
29444
29522
  function isNameTaken(name) {
29445
29523
  try {
@@ -29471,7 +29549,7 @@ function getAutoName() {
29471
29549
  }
29472
29550
  cachedAutoName = name;
29473
29551
  try {
29474
- mkdirSync2(dirname2(AGENT_ID_FILE), { recursive: true });
29552
+ mkdirSync3(dirname2(AGENT_ID_FILE), { recursive: true });
29475
29553
  writeFileSync(AGENT_ID_FILE, name + `
29476
29554
  `, "utf-8");
29477
29555
  } catch {}
@@ -29554,7 +29632,7 @@ function renameAgent(oldName, newName) {
29554
29632
  // package.json
29555
29633
  var package_default = {
29556
29634
  name: "@hasna/conversations",
29557
- version: "0.1.16",
29635
+ version: "0.1.18",
29558
29636
  description: "Real-time CLI messaging for AI agents",
29559
29637
  type: "module",
29560
29638
  bin: {
package/dist/index.d.ts CHANGED
@@ -16,5 +16,6 @@ export { createProject, listProjects, getProject, getProjectByName, updateProjec
16
16
  export { getDb, getDbPath, closeDb, } from "./lib/db.js";
17
17
  export { startPolling, useSpaceMessages, } from "./lib/poll.js";
18
18
  export { resolveIdentity, requireIdentity, } from "./lib/identity.js";
19
+ export { addReaction, removeReaction, getReactions, getReactionSummary, } from "./lib/reactions.js";
19
20
  export { heartbeat, getPresence, listAgents, removePresence, renameAgent, } from "./lib/presence.js";
20
- export type { Message, Session, Space, SpaceInfo, SpaceMember, Project, ProjectInfo, Priority, SendMessageOptions, ReadMessagesOptions, SearchMessagesOptions, AgentPresence, } from "./types.js";
21
+ export type { Message, Session, Space, SpaceInfo, SpaceMember, Project, ProjectInfo, Priority, SendMessageOptions, ReadMessagesOptions, SearchMessagesOptions, AgentPresence, Reaction, Attachment, } from "./types.js";
package/dist/index.js CHANGED
@@ -133,6 +133,17 @@ function getDb() {
133
133
  metadata TEXT
134
134
  )
135
135
  `);
136
+ db.exec(`
137
+ CREATE TABLE IF NOT EXISTS reactions (
138
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
139
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
140
+ agent TEXT NOT NULL,
141
+ emoji TEXT NOT NULL,
142
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
143
+ UNIQUE(message_id, agent, emoji)
144
+ )
145
+ `);
146
+ db.exec("CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id)");
136
147
  const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
137
148
  const tableNames = existingTables.map((t) => t.name);
138
149
  if (tableNames.includes("channels") && tableNames.includes("spaces")) {
@@ -190,6 +201,9 @@ function getDb() {
190
201
  db.exec("ALTER TABLE messages ADD COLUMN blocking INTEGER NOT NULL DEFAULT 0");
191
202
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_blocking ON messages(blocking)");
192
203
  }
204
+ if (!colNames2.includes("attachments")) {
205
+ db.exec("ALTER TABLE messages ADD COLUMN attachments TEXT");
206
+ }
193
207
  return db;
194
208
  }
195
209
  function closeDb() {
@@ -2015,6 +2029,9 @@ var require_react = __commonJS((exports, module) => {
2015
2029
  // src/lib/messages.ts
2016
2030
  init_db();
2017
2031
  import { randomUUID } from "crypto";
2032
+ import { mkdirSync as mkdirSync2, copyFileSync, statSync } from "fs";
2033
+ import { join as join2 } from "path";
2034
+ import { homedir as homedir2 } from "os";
2018
2035
  function parseMessage(row) {
2019
2036
  let metadata = null;
2020
2037
  if (row.metadata) {
@@ -2024,12 +2041,53 @@ function parseMessage(row) {
2024
2041
  metadata = null;
2025
2042
  }
2026
2043
  }
2044
+ let attachments = null;
2045
+ if (row.attachments) {
2046
+ try {
2047
+ attachments = JSON.parse(row.attachments);
2048
+ } catch {
2049
+ attachments = null;
2050
+ }
2051
+ }
2027
2052
  return {
2028
2053
  ...row,
2029
2054
  metadata,
2055
+ attachments,
2030
2056
  blocking: !!row.blocking
2031
2057
  };
2032
2058
  }
2059
+ function getAttachmentsDir() {
2060
+ if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
2061
+ return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
2062
+ return join2(homedir2(), ".conversations", "attachments");
2063
+ }
2064
+ function guessMimeType(name) {
2065
+ const ext = name.split(".").pop()?.toLowerCase();
2066
+ const mimeMap = {
2067
+ txt: "text/plain",
2068
+ md: "text/markdown",
2069
+ json: "application/json",
2070
+ js: "text/javascript",
2071
+ ts: "text/typescript",
2072
+ py: "text/x-python",
2073
+ html: "text/html",
2074
+ css: "text/css",
2075
+ xml: "application/xml",
2076
+ png: "image/png",
2077
+ jpg: "image/jpeg",
2078
+ jpeg: "image/jpeg",
2079
+ gif: "image/gif",
2080
+ svg: "image/svg+xml",
2081
+ webp: "image/webp",
2082
+ pdf: "application/pdf",
2083
+ zip: "application/zip",
2084
+ gz: "application/gzip",
2085
+ csv: "text/csv",
2086
+ yaml: "text/yaml",
2087
+ yml: "text/yaml"
2088
+ };
2089
+ return mimeMap[ext || ""] || "application/octet-stream";
2090
+ }
2033
2091
  function sendMessage(opts) {
2034
2092
  const db2 = getDb();
2035
2093
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
@@ -2043,7 +2101,27 @@ function sendMessage(opts) {
2043
2101
  RETURNING *
2044
2102
  `);
2045
2103
  const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking);
2046
- return parseMessage(row);
2104
+ const message = parseMessage(row);
2105
+ if (opts.attachments && opts.attachments.length > 0) {
2106
+ const attachmentsDir = join2(getAttachmentsDir(), String(message.id));
2107
+ mkdirSync2(attachmentsDir, { recursive: true });
2108
+ const attachmentInfos = [];
2109
+ for (const att of opts.attachments) {
2110
+ const destPath = join2(attachmentsDir, att.name);
2111
+ copyFileSync(att.source_path, destPath);
2112
+ const stat = statSync(destPath);
2113
+ attachmentInfos.push({
2114
+ name: att.name,
2115
+ path: destPath,
2116
+ size: stat.size,
2117
+ mime_type: guessMimeType(att.name)
2118
+ });
2119
+ }
2120
+ const attachmentsJson = JSON.stringify(attachmentInfos);
2121
+ db2.prepare("UPDATE messages SET attachments = ? WHERE id = ?").run(attachmentsJson, message.id);
2122
+ message.attachments = attachmentInfos;
2123
+ }
2124
+ return message;
2047
2125
  }
2048
2126
  function readMessages(opts = {}) {
2049
2127
  const db2 = getDb();
@@ -2720,9 +2798,9 @@ function useSpaceMessages(spaceName) {
2720
2798
  return messages;
2721
2799
  }
2722
2800
  // src/lib/identity.ts
2723
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
2724
- import { join as join2, dirname as dirname2 } from "path";
2725
- import { homedir as homedir2 } from "os";
2801
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
2802
+ import { join as join3, dirname as dirname2 } from "path";
2803
+ import { homedir as homedir3 } from "os";
2726
2804
 
2727
2805
  // src/lib/names.ts
2728
2806
  var AGENT_NAMES = [
@@ -3074,7 +3152,7 @@ var AGENT_NAMES = [
3074
3152
  ];
3075
3153
 
3076
3154
  // src/lib/identity.ts
3077
- var AGENT_ID_FILE = join2(homedir2(), ".conversations", "agent-id");
3155
+ var AGENT_ID_FILE = join3(homedir3(), ".conversations", "agent-id");
3078
3156
  var cachedAutoName = null;
3079
3157
  function isNameTaken(name) {
3080
3158
  try {
@@ -3106,7 +3184,7 @@ function getAutoName() {
3106
3184
  }
3107
3185
  cachedAutoName = name;
3108
3186
  try {
3109
- mkdirSync2(dirname2(AGENT_ID_FILE), { recursive: true });
3187
+ mkdirSync3(dirname2(AGENT_ID_FILE), { recursive: true });
3110
3188
  writeFileSync(AGENT_ID_FILE, name + `
3111
3189
  `, "utf-8");
3112
3190
  } catch {}
@@ -3130,6 +3208,45 @@ function requireIdentity(explicit) {
3130
3208
  return envValue;
3131
3209
  throw new Error("Agent identity required. Set CONVERSATIONS_AGENT_ID env var or pass --from flag.");
3132
3210
  }
3211
+ // src/lib/reactions.ts
3212
+ init_db();
3213
+ function addReaction(messageId, agent, emoji) {
3214
+ const db2 = getDb();
3215
+ const stmt = db2.prepare(`
3216
+ INSERT INTO reactions (message_id, agent, emoji)
3217
+ VALUES (?, ?, ?)
3218
+ ON CONFLICT (message_id, agent, emoji) DO UPDATE SET agent = agent
3219
+ RETURNING *
3220
+ `);
3221
+ const row = stmt.get(messageId, agent, emoji);
3222
+ return row;
3223
+ }
3224
+ function removeReaction(messageId, agent, emoji) {
3225
+ const db2 = getDb();
3226
+ const stmt = db2.prepare("DELETE FROM reactions WHERE message_id = ? AND agent = ? AND emoji = ?");
3227
+ const result = stmt.run(messageId, agent, emoji);
3228
+ return result.changes > 0;
3229
+ }
3230
+ function getReactions(messageId) {
3231
+ const db2 = getDb();
3232
+ const rows = db2.prepare("SELECT * FROM reactions WHERE message_id = ? ORDER BY created_at ASC, id ASC").all(messageId);
3233
+ return rows;
3234
+ }
3235
+ function getReactionSummary(messageId) {
3236
+ const db2 = getDb();
3237
+ const rows = db2.prepare(`
3238
+ SELECT emoji, GROUP_CONCAT(agent) as agents, COUNT(*) as count
3239
+ FROM reactions
3240
+ WHERE message_id = ?
3241
+ GROUP BY emoji
3242
+ ORDER BY count DESC, MIN(created_at) ASC
3243
+ `).all(messageId);
3244
+ return rows.map((row) => ({
3245
+ emoji: row.emoji,
3246
+ count: row.count,
3247
+ agents: row.agents.split(",")
3248
+ }));
3249
+ }
3133
3250
  // src/lib/presence.ts
3134
3251
  init_db();
3135
3252
  var ONLINE_THRESHOLD_SECONDS = 60;
@@ -3211,6 +3328,7 @@ export {
3211
3328
  resolveIdentity,
3212
3329
  requireIdentity,
3213
3330
  renameAgent,
3331
+ removeReaction,
3214
3332
  removePresence,
3215
3333
  readMessages,
3216
3334
  pinMessage,
@@ -3231,6 +3349,8 @@ export {
3231
3349
  getSpaceDepth,
3232
3350
  getSpace,
3233
3351
  getSession,
3352
+ getReactions,
3353
+ getReactionSummary,
3234
3354
  getProjectByName,
3235
3355
  getProject,
3236
3356
  getPresence,
@@ -3245,5 +3365,6 @@ export {
3245
3365
  createSpace,
3246
3366
  createProject,
3247
3367
  closeDb,
3248
- archiveSpace
3368
+ archiveSpace,
3369
+ addReaction
3249
3370
  };
@@ -0,0 +1,10 @@
1
+ import type { Reaction } from "../types.js";
2
+ export declare function addReaction(messageId: number, agent: string, emoji: string): Reaction;
3
+ export declare function removeReaction(messageId: number, agent: string, emoji: string): boolean;
4
+ export declare function getReactions(messageId: number): Reaction[];
5
+ export interface ReactionSummary {
6
+ emoji: string;
7
+ count: number;
8
+ agents: string[];
9
+ }
10
+ export declare function getReactionSummary(messageId: number): ReactionSummary[];
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -16,6 +16,20 @@ export interface Message {
16
16
  edited_at: string | null;
17
17
  pinned_at: string | null;
18
18
  blocking: boolean;
19
+ attachments: Attachment[] | null;
20
+ }
21
+ export interface Reaction {
22
+ id: number;
23
+ message_id: number;
24
+ agent: string;
25
+ emoji: string;
26
+ created_at: string;
27
+ }
28
+ export interface Attachment {
29
+ name: string;
30
+ path: string;
31
+ size: number;
32
+ mime_type: string;
19
33
  }
20
34
  export interface Session {
21
35
  session_id: string;
@@ -71,6 +85,10 @@ export interface SendMessageOptions {
71
85
  branch?: string;
72
86
  metadata?: Record<string, unknown>;
73
87
  blocking?: boolean;
88
+ attachments?: {
89
+ name: string;
90
+ source_path: string;
91
+ }[];
74
92
  }
75
93
  export interface ReadMessagesOptions {
76
94
  session_id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {