@hasna/conversations 0.0.7 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -1892,7 +1892,7 @@ function getDb() {
1892
1892
  session_id TEXT NOT NULL,
1893
1893
  from_agent TEXT NOT NULL,
1894
1894
  to_agent TEXT NOT NULL,
1895
- channel TEXT,
1895
+ space TEXT,
1896
1896
  content TEXT NOT NULL,
1897
1897
  priority TEXT NOT NULL DEFAULT 'normal',
1898
1898
  working_dir TEXT,
@@ -1906,27 +1906,106 @@ function getDb() {
1906
1906
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
1907
1907
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
1908
1908
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
1909
- db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
1910
- const cols = db.prepare("PRAGMA table_info(messages)").all();
1911
- if (!cols.some((c) => c.name === "channel")) {
1912
- db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
1913
- }
1909
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
1910
+ db.exec(`
1911
+ CREATE TABLE IF NOT EXISTS projects (
1912
+ id TEXT PRIMARY KEY,
1913
+ name TEXT NOT NULL UNIQUE,
1914
+ description TEXT,
1915
+ path TEXT,
1916
+ created_by TEXT NOT NULL,
1917
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1918
+ metadata TEXT,
1919
+ tags TEXT,
1920
+ status TEXT NOT NULL DEFAULT 'active',
1921
+ repository TEXT,
1922
+ settings TEXT
1923
+ )
1924
+ `);
1925
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name)");
1926
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)");
1914
1927
  db.exec(`
1915
- CREATE TABLE IF NOT EXISTS channels (
1928
+ CREATE TABLE IF NOT EXISTS spaces (
1916
1929
  name TEXT PRIMARY KEY,
1917
1930
  description TEXT,
1931
+ parent_id TEXT REFERENCES spaces(name),
1932
+ project_id TEXT REFERENCES projects(id),
1918
1933
  created_by TEXT NOT NULL,
1919
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
1934
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1935
+ archived_at TEXT
1920
1936
  )
1921
1937
  `);
1938
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_parent ON spaces(parent_id)");
1939
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_project ON spaces(project_id)");
1922
1940
  db.exec(`
1923
- CREATE TABLE IF NOT EXISTS channel_members (
1924
- channel TEXT NOT NULL REFERENCES channels(name),
1941
+ CREATE TABLE IF NOT EXISTS space_members (
1942
+ space TEXT NOT NULL REFERENCES spaces(name),
1925
1943
  agent TEXT NOT NULL,
1926
1944
  joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1927
- PRIMARY KEY (channel, agent)
1945
+ PRIMARY KEY (space, agent)
1946
+ )
1947
+ `);
1948
+ db.exec(`
1949
+ CREATE TABLE IF NOT EXISTS agent_presence (
1950
+ agent TEXT PRIMARY KEY,
1951
+ status TEXT NOT NULL DEFAULT 'online',
1952
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1953
+ metadata TEXT
1928
1954
  )
1929
1955
  `);
1956
+ const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
1957
+ const tableNames = existingTables.map((t) => t.name);
1958
+ if (tableNames.includes("channels") && tableNames.includes("spaces")) {
1959
+ const spaceCount = db.prepare("SELECT COUNT(*) as c FROM spaces").get().c;
1960
+ const channelCount = db.prepare("SELECT COUNT(*) as c FROM channels").get().c;
1961
+ if (channelCount > 0 && spaceCount === 0) {
1962
+ db.exec("BEGIN");
1963
+ try {
1964
+ db.exec(`
1965
+ INSERT OR IGNORE INTO spaces (name, description, created_by, created_at)
1966
+ SELECT name, description, created_by, created_at FROM channels
1967
+ `);
1968
+ if (tableNames.includes("channel_members")) {
1969
+ db.exec(`
1970
+ INSERT OR IGNORE INTO space_members (space, agent, joined_at)
1971
+ SELECT channel, agent, joined_at FROM channel_members
1972
+ `);
1973
+ }
1974
+ db.exec("COMMIT");
1975
+ } catch (e) {
1976
+ db.exec("ROLLBACK");
1977
+ throw e;
1978
+ }
1979
+ }
1980
+ db.exec("DROP TABLE IF EXISTS channel_members");
1981
+ db.exec("DROP TABLE IF EXISTS channels");
1982
+ }
1983
+ const msgCols = db.prepare("PRAGMA table_info(messages)").all();
1984
+ const colNames = msgCols.map((c) => c.name);
1985
+ if (colNames.includes("channel") && !colNames.includes("space")) {
1986
+ db.exec("ALTER TABLE messages ADD COLUMN space TEXT");
1987
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
1988
+ db.exec("UPDATE messages SET space = channel WHERE channel IS NOT NULL");
1989
+ db.exec(`
1990
+ UPDATE messages
1991
+ SET session_id = 'space:' || substr(session_id, 9)
1992
+ WHERE session_id LIKE 'channel:%'
1993
+ `);
1994
+ }
1995
+ const spaceCols = db.prepare("PRAGMA table_info(spaces)").all();
1996
+ const spaceColNames = spaceCols.map((c) => c.name);
1997
+ if (!spaceColNames.includes("archived_at")) {
1998
+ db.exec("ALTER TABLE spaces ADD COLUMN archived_at TEXT");
1999
+ }
2000
+ const msgCols2 = db.prepare("PRAGMA table_info(messages)").all();
2001
+ const colNames2 = msgCols2.map((c) => c.name);
2002
+ if (!colNames2.includes("edited_at")) {
2003
+ db.exec("ALTER TABLE messages ADD COLUMN edited_at TEXT");
2004
+ }
2005
+ if (!colNames2.includes("pinned_at")) {
2006
+ db.exec("ALTER TABLE messages ADD COLUMN pinned_at TEXT");
2007
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_pinned ON messages(pinned_at)");
2008
+ }
1930
2009
  return db;
1931
2010
  }
1932
2011
  function closeDb() {
@@ -1941,21 +2020,31 @@ var init_db = () => {};
1941
2020
  // src/lib/messages.ts
1942
2021
  import { randomUUID } from "crypto";
1943
2022
  function parseMessage(row) {
2023
+ let metadata = null;
2024
+ if (row.metadata) {
2025
+ try {
2026
+ metadata = JSON.parse(row.metadata);
2027
+ } catch {
2028
+ metadata = null;
2029
+ }
2030
+ }
1944
2031
  return {
1945
2032
  ...row,
1946
- metadata: row.metadata ? JSON.parse(row.metadata) : null
2033
+ metadata
1947
2034
  };
1948
2035
  }
1949
2036
  function sendMessage(opts) {
1950
2037
  const db2 = getDb();
1951
- const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
2038
+ const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
2039
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`);
1952
2040
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
2041
+ const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
1953
2042
  const stmt = db2.prepare(`
1954
- INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
2043
+ INSERT INTO messages (session_id, from_agent, to_agent, space, content, priority, working_dir, repository, branch, metadata)
1955
2044
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1956
2045
  RETURNING *
1957
2046
  `);
1958
- const row = stmt.get(sessionId, opts.from, opts.to, opts.channel || null, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
2047
+ 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);
1959
2048
  return parseMessage(row);
1960
2049
  }
1961
2050
  function readMessages(opts = {}) {
@@ -1974,20 +2063,25 @@ function readMessages(opts = {}) {
1974
2063
  conditions.push("to_agent = ?");
1975
2064
  params.push(opts.to);
1976
2065
  }
1977
- if (opts.channel) {
1978
- conditions.push("channel = ?");
1979
- params.push(opts.channel);
2066
+ if (opts.space) {
2067
+ conditions.push("space = ?");
2068
+ params.push(opts.space);
1980
2069
  }
1981
2070
  if (opts.since) {
1982
2071
  conditions.push("created_at > ?");
1983
2072
  params.push(opts.since);
1984
2073
  }
2074
+ if (opts.since_id !== undefined) {
2075
+ conditions.push("id > ?");
2076
+ params.push(opts.since_id);
2077
+ }
1985
2078
  if (opts.unread_only) {
1986
2079
  conditions.push("read_at IS NULL");
1987
2080
  }
1988
2081
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1989
- const limit = opts.limit ? `LIMIT ${opts.limit}` : "";
1990
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC ${limit}`).all(...params);
2082
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
2083
+ const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
2084
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} ${limit}`).all(...params);
1991
2085
  return rows.map(parseMessage);
1992
2086
  }
1993
2087
  function markRead(ids, reader) {
@@ -2005,10 +2099,10 @@ function markSessionRead(sessionId, reader) {
2005
2099
  const result = stmt.run(sessionId, reader);
2006
2100
  return result.changes;
2007
2101
  }
2008
- function markChannelRead(channelName, reader) {
2102
+ function markSpaceRead(spaceName, reader) {
2009
2103
  const db2 = getDb();
2010
- const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE channel = ? AND from_agent != ? AND read_at IS NULL`);
2011
- const result = stmt.run(channelName, reader);
2104
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE space = ? AND from_agent != ? AND read_at IS NULL`);
2105
+ const result = stmt.run(spaceName, reader);
2012
2106
  return result.changes;
2013
2107
  }
2014
2108
  function getMessageById(id) {
@@ -2016,6 +2110,130 @@ function getMessageById(id) {
2016
2110
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
2017
2111
  return row ? parseMessage(row) : null;
2018
2112
  }
2113
+ function markAllRead(agent) {
2114
+ const db2 = getDb();
2115
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE to_agent = ? AND read_at IS NULL`);
2116
+ const result = stmt.run(agent);
2117
+ return result.changes;
2118
+ }
2119
+ function escapeCsvField(value) {
2120
+ if (value === null || value === undefined)
2121
+ return "";
2122
+ const str = String(value);
2123
+ if (str.includes(",") || str.includes('"') || str.includes(`
2124
+ `) || str.includes("\r")) {
2125
+ return `"${str.replace(/"/g, '""')}"`;
2126
+ }
2127
+ return str;
2128
+ }
2129
+ function exportMessages(opts) {
2130
+ const db2 = getDb();
2131
+ const conditions = [];
2132
+ const params = [];
2133
+ if (opts?.space) {
2134
+ conditions.push("space = ?");
2135
+ params.push(opts.space);
2136
+ }
2137
+ if (opts?.session_id) {
2138
+ conditions.push("session_id = ?");
2139
+ params.push(opts.session_id);
2140
+ }
2141
+ if (opts?.from) {
2142
+ conditions.push("from_agent = ?");
2143
+ params.push(opts.from);
2144
+ }
2145
+ if (opts?.since) {
2146
+ conditions.push("created_at >= ?");
2147
+ params.push(opts.since);
2148
+ }
2149
+ if (opts?.until) {
2150
+ conditions.push("created_at <= ?");
2151
+ params.push(opts.until);
2152
+ }
2153
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2154
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC, id ASC`).all(...params);
2155
+ const messages = rows.map(parseMessage);
2156
+ const format = opts?.format ?? "json";
2157
+ if (format === "csv") {
2158
+ const headers = "id,session_id,from_agent,to_agent,space,content,priority,created_at,read_at";
2159
+ const lines = messages.map((m) => [
2160
+ String(m.id),
2161
+ escapeCsvField(m.session_id),
2162
+ escapeCsvField(m.from_agent),
2163
+ escapeCsvField(m.to_agent),
2164
+ escapeCsvField(m.space),
2165
+ escapeCsvField(m.content),
2166
+ escapeCsvField(m.priority),
2167
+ escapeCsvField(m.created_at),
2168
+ escapeCsvField(m.read_at)
2169
+ ].join(","));
2170
+ return [headers, ...lines].join(`
2171
+ `);
2172
+ }
2173
+ return JSON.stringify(messages, null, 2);
2174
+ }
2175
+ function deleteMessage(id, agent) {
2176
+ const db2 = getDb();
2177
+ const stmt = db2.prepare("DELETE FROM messages WHERE id = ? AND from_agent = ?");
2178
+ const result = stmt.run(id, agent);
2179
+ return result.changes > 0;
2180
+ }
2181
+ function editMessage(id, agent, newContent) {
2182
+ const db2 = getDb();
2183
+ const stmt = db2.prepare(`UPDATE messages SET content = ?, edited_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? AND from_agent = ? RETURNING *`);
2184
+ const row = stmt.get(newContent, id, agent);
2185
+ return row ? parseMessage(row) : null;
2186
+ }
2187
+ function pinMessage(id) {
2188
+ const db2 = getDb();
2189
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? RETURNING *`);
2190
+ const row = stmt.get(id);
2191
+ return row ? parseMessage(row) : null;
2192
+ }
2193
+ function unpinMessage(id) {
2194
+ const db2 = getDb();
2195
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = NULL WHERE id = ? RETURNING *`);
2196
+ const row = stmt.get(id);
2197
+ return row ? parseMessage(row) : null;
2198
+ }
2199
+ function getPinnedMessages(opts) {
2200
+ const db2 = getDb();
2201
+ const conditions = ["pinned_at IS NOT NULL"];
2202
+ const params = [];
2203
+ if (opts?.space) {
2204
+ conditions.push("space = ?");
2205
+ params.push(opts.space);
2206
+ }
2207
+ if (opts?.session_id) {
2208
+ conditions.push("session_id = ?");
2209
+ params.push(opts.session_id);
2210
+ }
2211
+ const where = `WHERE ${conditions.join(" AND ")}`;
2212
+ const limit = Number.isFinite(opts?.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
2213
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limit}`).all(...params);
2214
+ return rows.map(parseMessage);
2215
+ }
2216
+ function searchMessages(opts) {
2217
+ const db2 = getDb();
2218
+ const conditions = ["content LIKE ?"];
2219
+ const params = [`%${opts.query}%`];
2220
+ if (opts.space) {
2221
+ conditions.push("space = ?");
2222
+ params.push(opts.space);
2223
+ }
2224
+ if (opts.from) {
2225
+ conditions.push("from_agent = ?");
2226
+ params.push(opts.from);
2227
+ }
2228
+ if (opts.to) {
2229
+ conditions.push("to_agent = ?");
2230
+ params.push(opts.to);
2231
+ }
2232
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
2233
+ const where = `WHERE ${conditions.join(" AND ")}`;
2234
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
2235
+ return rows.map(parseMessage);
2236
+ }
2019
2237
  var init_messages = __esm(() => {
2020
2238
  init_db();
2021
2239
  });
@@ -2053,73 +2271,493 @@ var init_sessions = __esm(() => {
2053
2271
  init_db();
2054
2272
  });
2055
2273
 
2056
- // src/lib/channels.ts
2057
- function createChannel(name, createdBy, description) {
2274
+ // src/lib/spaces.ts
2275
+ function getSpaceDepth(spaceName) {
2276
+ const db2 = getDb();
2277
+ let depth = 0;
2278
+ let current = spaceName;
2279
+ for (let i = 0;i < 10; i++) {
2280
+ const row = db2.prepare("SELECT parent_id FROM spaces WHERE name = ?").get(current);
2281
+ if (!row || !row.parent_id)
2282
+ break;
2283
+ depth++;
2284
+ current = row.parent_id;
2285
+ }
2286
+ return depth;
2287
+ }
2288
+ function createSpace(name, createdBy, options) {
2058
2289
  const db2 = getDb();
2059
- const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
2060
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
2290
+ if (options?.parent_id) {
2291
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(options.parent_id);
2292
+ if (!parentExists) {
2293
+ throw new Error(`Parent space not found: ${options.parent_id}`);
2294
+ }
2295
+ const parentDepth = getSpaceDepth(options.parent_id);
2296
+ if (parentDepth >= 2) {
2297
+ throw new Error("Maximum space nesting depth is 3 levels");
2298
+ }
2299
+ }
2300
+ if (options?.project_id) {
2301
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(options.project_id);
2302
+ if (!projectExists) {
2303
+ throw new Error(`Project not found: ${options.project_id}`);
2304
+ }
2305
+ }
2306
+ const row = db2.prepare("INSERT INTO spaces (name, description, parent_id, project_id, created_by) VALUES (?, ?, ?, ?, ?) RETURNING *").get(name, options?.description || null, options?.parent_id || null, options?.project_id || null, createdBy);
2307
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(name, createdBy);
2061
2308
  return row;
2062
2309
  }
2063
- function listChannels() {
2310
+ function listSpaces(options) {
2064
2311
  const db2 = getDb();
2312
+ const conditions = [];
2313
+ const params = [];
2314
+ if (options?.project_id) {
2315
+ conditions.push("s.project_id = ?");
2316
+ params.push(options.project_id);
2317
+ }
2318
+ if (options?.parent_id !== undefined) {
2319
+ if (options.parent_id === null) {
2320
+ conditions.push("s.parent_id IS NULL");
2321
+ } else {
2322
+ conditions.push("s.parent_id = ?");
2323
+ params.push(options.parent_id);
2324
+ }
2325
+ }
2326
+ if (!options?.include_archived) {
2327
+ conditions.push("s.archived_at IS NULL");
2328
+ }
2329
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2065
2330
  const rows = db2.prepare(`
2066
2331
  SELECT
2067
- c.name,
2068
- c.description,
2069
- c.created_by,
2070
- c.created_at,
2071
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2072
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2073
- FROM channels c
2074
- ORDER BY c.name ASC
2075
- `).all();
2332
+ s.name,
2333
+ s.description,
2334
+ s.parent_id,
2335
+ s.project_id,
2336
+ s.created_by,
2337
+ s.created_at,
2338
+ s.archived_at,
2339
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
2340
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
2341
+ FROM spaces s
2342
+ ${where}
2343
+ ORDER BY s.name ASC
2344
+ `).all(...params);
2076
2345
  return rows;
2077
2346
  }
2078
- function getChannel(name) {
2347
+ function getSpace(name) {
2079
2348
  const db2 = getDb();
2080
2349
  const row = db2.prepare(`
2081
2350
  SELECT
2082
- c.name,
2083
- c.description,
2084
- c.created_by,
2085
- c.created_at,
2086
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2087
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2088
- FROM channels c
2089
- WHERE c.name = ?
2351
+ s.name,
2352
+ s.description,
2353
+ s.parent_id,
2354
+ s.project_id,
2355
+ s.created_by,
2356
+ s.created_at,
2357
+ s.archived_at,
2358
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
2359
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
2360
+ FROM spaces s
2361
+ WHERE s.name = ?
2090
2362
  `).get(name);
2091
2363
  return row;
2092
2364
  }
2093
- function joinChannel(channelName, agent) {
2365
+ function joinSpace(spaceName, agent) {
2094
2366
  const db2 = getDb();
2095
- const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
2096
- if (!channel)
2367
+ const space = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(spaceName);
2368
+ if (!space)
2097
2369
  return false;
2098
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
2370
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(spaceName, agent);
2099
2371
  return true;
2100
2372
  }
2101
- function leaveChannel(channelName, agent) {
2373
+ function leaveSpace(spaceName, agent) {
2102
2374
  const db2 = getDb();
2103
- const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
2375
+ const result = db2.prepare("DELETE FROM space_members WHERE space = ? AND agent = ?").run(spaceName, agent);
2104
2376
  return result.changes > 0;
2105
2377
  }
2106
- function getChannelMembers(channelName) {
2378
+ function getSpaceMembers(spaceName) {
2379
+ const db2 = getDb();
2380
+ return db2.prepare("SELECT space, agent, joined_at FROM space_members WHERE space = ? ORDER BY joined_at ASC").all(spaceName);
2381
+ }
2382
+ function updateSpace(name, updates) {
2383
+ const db2 = getDb();
2384
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2385
+ if (!existing) {
2386
+ throw new Error(`Space not found: ${name}`);
2387
+ }
2388
+ if (updates.parent_id !== undefined && updates.parent_id !== existing.parent_id) {
2389
+ if (updates.parent_id !== null) {
2390
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(updates.parent_id);
2391
+ if (!parentExists) {
2392
+ throw new Error(`Parent space not found: ${updates.parent_id}`);
2393
+ }
2394
+ const parentDepth = getSpaceDepth(updates.parent_id);
2395
+ if (parentDepth >= 2) {
2396
+ throw new Error("Maximum space nesting depth is 3 levels");
2397
+ }
2398
+ if (updates.parent_id === name) {
2399
+ throw new Error("A space cannot be its own parent");
2400
+ }
2401
+ }
2402
+ }
2403
+ if (updates.project_id !== undefined && updates.project_id !== existing.project_id) {
2404
+ if (updates.project_id !== null) {
2405
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(updates.project_id);
2406
+ if (!projectExists) {
2407
+ throw new Error(`Project not found: ${updates.project_id}`);
2408
+ }
2409
+ }
2410
+ }
2411
+ const sets = [];
2412
+ const params = [];
2413
+ if (updates.description !== undefined) {
2414
+ sets.push("description = ?");
2415
+ params.push(updates.description);
2416
+ }
2417
+ if (updates.parent_id !== undefined) {
2418
+ sets.push("parent_id = ?");
2419
+ params.push(updates.parent_id);
2420
+ }
2421
+ if (updates.project_id !== undefined) {
2422
+ sets.push("project_id = ?");
2423
+ params.push(updates.project_id);
2424
+ }
2425
+ if (sets.length === 0) {
2426
+ return existing;
2427
+ }
2428
+ params.push(name);
2429
+ const row = db2.prepare(`UPDATE spaces SET ${sets.join(", ")} WHERE name = ? RETURNING *`).get(...params);
2430
+ return row;
2431
+ }
2432
+ function archiveSpace(name) {
2433
+ const db2 = getDb();
2434
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2435
+ if (!existing) {
2436
+ throw new Error(`Space not found: ${name}`);
2437
+ }
2438
+ const row = db2.prepare("UPDATE spaces SET archived_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE name = ? RETURNING *").get(name);
2439
+ return row;
2440
+ }
2441
+ function unarchiveSpace(name) {
2442
+ const db2 = getDb();
2443
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2444
+ if (!existing) {
2445
+ throw new Error(`Space not found: ${name}`);
2446
+ }
2447
+ const row = db2.prepare("UPDATE spaces SET archived_at = NULL WHERE name = ? RETURNING *").get(name);
2448
+ return row;
2449
+ }
2450
+ var init_spaces = __esm(() => {
2451
+ init_db();
2452
+ });
2453
+
2454
+ // src/lib/projects.ts
2455
+ import { randomUUID as randomUUID2 } from "crypto";
2456
+ function parseProject(row) {
2457
+ let metadata = null;
2458
+ if (row.metadata) {
2459
+ try {
2460
+ metadata = JSON.parse(row.metadata);
2461
+ } catch {
2462
+ metadata = null;
2463
+ }
2464
+ }
2465
+ let tags = [];
2466
+ if (row.tags) {
2467
+ try {
2468
+ tags = JSON.parse(row.tags);
2469
+ } catch {
2470
+ tags = [];
2471
+ }
2472
+ }
2473
+ let settings = null;
2474
+ if (row.settings) {
2475
+ try {
2476
+ settings = JSON.parse(row.settings);
2477
+ } catch {
2478
+ settings = null;
2479
+ }
2480
+ }
2481
+ return {
2482
+ id: row.id,
2483
+ name: row.name,
2484
+ description: row.description || null,
2485
+ path: row.path || null,
2486
+ created_by: row.created_by,
2487
+ created_at: row.created_at,
2488
+ metadata,
2489
+ tags,
2490
+ status: row.status || "active",
2491
+ repository: row.repository || null,
2492
+ settings
2493
+ };
2494
+ }
2495
+ function createProject(opts) {
2496
+ const db2 = getDb();
2497
+ const id = randomUUID2();
2498
+ const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
2499
+ const tags = opts.tags ? JSON.stringify(opts.tags) : null;
2500
+ const settings = opts.settings ? JSON.stringify(opts.settings) : null;
2501
+ const row = db2.prepare(`
2502
+ INSERT INTO projects (id, name, description, path, created_by, metadata, tags, repository, settings)
2503
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2504
+ RETURNING *
2505
+ `).get(id, opts.name, opts.description || null, opts.path || null, opts.created_by, metadata, tags, opts.repository || null, settings);
2506
+ return parseProject(row);
2507
+ }
2508
+ function listProjects(opts) {
2509
+ const db2 = getDb();
2510
+ const conditions = [];
2511
+ const params = [];
2512
+ if (opts?.status) {
2513
+ conditions.push("p.status = ?");
2514
+ params.push(opts.status);
2515
+ }
2516
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2517
+ const rows = db2.prepare(`
2518
+ SELECT
2519
+ p.*,
2520
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2521
+ FROM projects p
2522
+ ${where}
2523
+ ORDER BY p.name ASC
2524
+ `).all(...params);
2525
+ return rows.map((row) => ({
2526
+ ...parseProject(row),
2527
+ space_count: row.space_count
2528
+ }));
2529
+ }
2530
+ function getProject(id) {
2531
+ const db2 = getDb();
2532
+ const row = db2.prepare(`
2533
+ SELECT
2534
+ p.*,
2535
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2536
+ FROM projects p
2537
+ WHERE p.id = ?
2538
+ `).get(id);
2539
+ if (!row)
2540
+ return null;
2541
+ return {
2542
+ ...parseProject(row),
2543
+ space_count: row.space_count
2544
+ };
2545
+ }
2546
+ function getProjectByName(name) {
2547
+ const db2 = getDb();
2548
+ const row = db2.prepare(`
2549
+ SELECT
2550
+ p.*,
2551
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2552
+ FROM projects p
2553
+ WHERE p.name = ?
2554
+ `).get(name);
2555
+ if (!row)
2556
+ return null;
2557
+ return {
2558
+ ...parseProject(row),
2559
+ space_count: row.space_count
2560
+ };
2561
+ }
2562
+ function updateProject(id, updates) {
2563
+ const db2 = getDb();
2564
+ const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(id);
2565
+ if (!existing) {
2566
+ throw new Error(`Project not found: ${id}`);
2567
+ }
2568
+ const sets = [];
2569
+ const params = [];
2570
+ if (updates.name !== undefined) {
2571
+ sets.push("name = ?");
2572
+ params.push(updates.name);
2573
+ }
2574
+ if (updates.description !== undefined) {
2575
+ sets.push("description = ?");
2576
+ params.push(updates.description);
2577
+ }
2578
+ if (updates.path !== undefined) {
2579
+ sets.push("path = ?");
2580
+ params.push(updates.path);
2581
+ }
2582
+ if (updates.metadata !== undefined) {
2583
+ sets.push("metadata = ?");
2584
+ params.push(JSON.stringify(updates.metadata));
2585
+ }
2586
+ if (updates.tags !== undefined) {
2587
+ sets.push("tags = ?");
2588
+ params.push(JSON.stringify(updates.tags));
2589
+ }
2590
+ if (updates.status !== undefined) {
2591
+ sets.push("status = ?");
2592
+ params.push(updates.status);
2593
+ }
2594
+ if (updates.repository !== undefined) {
2595
+ sets.push("repository = ?");
2596
+ params.push(updates.repository);
2597
+ }
2598
+ if (updates.settings !== undefined) {
2599
+ sets.push("settings = ?");
2600
+ params.push(JSON.stringify(updates.settings));
2601
+ }
2602
+ if (sets.length === 0) {
2603
+ return parseProject(existing);
2604
+ }
2605
+ params.push(id);
2606
+ const row = db2.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
2607
+ return parseProject(row);
2608
+ }
2609
+ function deleteProject(id) {
2107
2610
  const db2 = getDb();
2108
- return db2.prepare("SELECT channel, agent, joined_at FROM channel_members WHERE channel = ? ORDER BY joined_at ASC").all(channelName);
2611
+ const spaceCount = db2.prepare("SELECT COUNT(*) as c FROM spaces WHERE project_id = ?").get(id).c;
2612
+ if (spaceCount > 0) {
2613
+ throw new Error(`Cannot delete project: ${spaceCount} space(s) still reference it`);
2614
+ }
2615
+ const result = db2.prepare("DELETE FROM projects WHERE id = ?").run(id);
2616
+ return result.changes > 0;
2109
2617
  }
2110
- var init_channels = __esm(() => {
2618
+ var init_projects = __esm(() => {
2111
2619
  init_db();
2112
2620
  });
2113
2621
 
2114
2622
  // src/lib/identity.ts
2115
2623
  function resolveIdentity(explicit) {
2116
- if (explicit)
2117
- return explicit;
2118
- if (process.env.CONVERSATIONS_AGENT_ID)
2119
- return process.env.CONVERSATIONS_AGENT_ID;
2624
+ const explicitValue = explicit?.trim();
2625
+ if (explicitValue)
2626
+ return explicitValue;
2627
+ const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
2628
+ if (envValue)
2629
+ return envValue;
2120
2630
  return "user";
2121
2631
  }
2122
2632
 
2633
+ // src/lib/presence.ts
2634
+ function parsePresence(row) {
2635
+ let metadata = null;
2636
+ if (row.metadata) {
2637
+ try {
2638
+ metadata = JSON.parse(row.metadata);
2639
+ } catch {
2640
+ metadata = null;
2641
+ }
2642
+ }
2643
+ const lastSeenAt = row.last_seen_at;
2644
+ const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
2645
+ const nowMs = Date.now();
2646
+ const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
2647
+ return {
2648
+ agent: row.agent,
2649
+ status: row.status,
2650
+ last_seen_at: lastSeenAt,
2651
+ online,
2652
+ metadata
2653
+ };
2654
+ }
2655
+ function heartbeat(agent, status, metadata) {
2656
+ const db2 = getDb();
2657
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
2658
+ const resolvedStatus = status || "online";
2659
+ db2.prepare(`
2660
+ INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
2661
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
2662
+ ON CONFLICT(agent) DO UPDATE SET
2663
+ status = excluded.status,
2664
+ last_seen_at = excluded.last_seen_at,
2665
+ metadata = excluded.metadata
2666
+ `).run(agent, resolvedStatus, metadataJson);
2667
+ }
2668
+ function listAgents(opts) {
2669
+ const db2 = getDb();
2670
+ let query = "SELECT * FROM agent_presence";
2671
+ const params = [];
2672
+ if (opts?.online_only) {
2673
+ query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
2674
+ }
2675
+ query += " ORDER BY last_seen_at DESC";
2676
+ const rows = db2.prepare(query).all(...params);
2677
+ return rows.map(parsePresence);
2678
+ }
2679
+ var ONLINE_THRESHOLD_SECONDS = 60;
2680
+ var init_presence = __esm(() => {
2681
+ init_db();
2682
+ });
2683
+
2684
+ // package.json
2685
+ var require_package = __commonJS((exports, module) => {
2686
+ module.exports = {
2687
+ name: "@hasna/conversations",
2688
+ version: "0.1.0",
2689
+ description: "Real-time CLI messaging for AI agents",
2690
+ type: "module",
2691
+ bin: {
2692
+ conversations: "bin/index.js",
2693
+ "conversations-mcp": "bin/mcp.js"
2694
+ },
2695
+ exports: {
2696
+ ".": {
2697
+ import: "./dist/index.js",
2698
+ types: "./dist/index.d.ts"
2699
+ }
2700
+ },
2701
+ files: [
2702
+ "dist/",
2703
+ "bin/",
2704
+ "dashboard/dist/",
2705
+ "LICENSE",
2706
+ "README.md"
2707
+ ],
2708
+ main: "./dist/index.js",
2709
+ types: "./dist/index.d.ts",
2710
+ scripts: {
2711
+ build: "bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
2712
+ "build:dashboard": "cd dashboard && bun install && bun run build",
2713
+ test: "bun test",
2714
+ dev: "bun run ./src/cli/index.tsx",
2715
+ typecheck: "tsc --noEmit",
2716
+ prepublishOnly: "bun run build"
2717
+ },
2718
+ keywords: [
2719
+ "conversations",
2720
+ "messaging",
2721
+ "ai",
2722
+ "agent",
2723
+ "cli",
2724
+ "typescript",
2725
+ "bun",
2726
+ "claude",
2727
+ "mcp"
2728
+ ],
2729
+ author: "Hasna",
2730
+ license: "Apache-2.0",
2731
+ devDependencies: {
2732
+ "@types/bun": "latest",
2733
+ "@types/react": "^18.2.0",
2734
+ typescript: "^5"
2735
+ },
2736
+ dependencies: {
2737
+ "@modelcontextprotocol/sdk": "^1.26.0",
2738
+ chalk: "^5.3.0",
2739
+ commander: "^12.1.0",
2740
+ ink: "^5.0.1",
2741
+ "ink-select-input": "^6.0.0",
2742
+ "ink-spinner": "^5.0.0",
2743
+ "ink-text-input": "^6.0.0",
2744
+ react: "^18.2.0",
2745
+ zod: "^4.3.6"
2746
+ },
2747
+ engines: {
2748
+ bun: ">=1.0.0"
2749
+ },
2750
+ publishConfig: {
2751
+ registry: "https://registry.npmjs.org",
2752
+ access: "public"
2753
+ },
2754
+ repository: {
2755
+ type: "git",
2756
+ url: "git+https://github.com/hasna/conversations.git"
2757
+ }
2758
+ };
2759
+ });
2760
+
2123
2761
  // node_modules/zod/v3/helpers/util.js
2124
2762
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
2125
2763
  const t = typeof data;
@@ -29410,7 +30048,7 @@ var require_formats = __commonJS((exports) => {
29410
30048
  }
29411
30049
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
29412
30050
  function getTime(strictTimeZone) {
29413
- return function time3(str) {
30051
+ return function time(str) {
29414
30052
  const matches = TIME.exec(str);
29415
30053
  if (!matches)
29416
30054
  return false;
@@ -31002,10 +31640,12 @@ var init_mcp2 = __esm(() => {
31002
31640
  init_zod();
31003
31641
  init_messages();
31004
31642
  init_sessions();
31005
- init_channels();
31643
+ init_spaces();
31644
+ init_projects();
31645
+ init_presence();
31006
31646
  server = new McpServer({
31007
31647
  name: "conversations",
31008
- version: "0.0.7"
31648
+ version: "0.1.0"
31009
31649
  });
31010
31650
  server.registerTool("send_message", {
31011
31651
  title: "Send Message",
@@ -31022,7 +31662,17 @@ var init_mcp2 = __esm(() => {
31022
31662
  }
31023
31663
  }, async ({ to, content, session_id, priority, working_dir, repository, branch, metadata }) => {
31024
31664
  const from = resolveIdentity();
31025
- const parsedMetadata = metadata ? JSON.parse(metadata) : undefined;
31665
+ let parsedMetadata;
31666
+ if (metadata) {
31667
+ try {
31668
+ parsedMetadata = JSON.parse(metadata);
31669
+ } catch {
31670
+ return {
31671
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
31672
+ isError: true
31673
+ };
31674
+ }
31675
+ }
31026
31676
  const msg = sendMessage({
31027
31677
  from,
31028
31678
  to,
@@ -31045,7 +31695,7 @@ var init_mcp2 = __esm(() => {
31045
31695
  session_id: exports_external.string().optional().describe("Filter by session ID"),
31046
31696
  from: exports_external.string().optional().describe("Filter by sender agent ID"),
31047
31697
  to: exports_external.string().optional().describe("Filter by recipient agent ID"),
31048
- channel: exports_external.string().optional().describe("Filter by channel name"),
31698
+ space: exports_external.string().optional().describe("Filter by space name"),
31049
31699
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
31050
31700
  limit: exports_external.number().optional().describe("Max messages to return"),
31051
31701
  unread_only: exports_external.boolean().optional().describe("Only return unread messages")
@@ -31085,12 +31735,15 @@ var init_mcp2 = __esm(() => {
31085
31735
  };
31086
31736
  }
31087
31737
  const from = resolveIdentity();
31738
+ const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
31739
+ const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
31088
31740
  const msg = sendMessage({
31089
31741
  from,
31090
- to: original.from_agent,
31742
+ to,
31091
31743
  content,
31092
31744
  session_id: original.session_id,
31093
- priority
31745
+ priority,
31746
+ space
31094
31747
  });
31095
31748
  return {
31096
31749
  content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
@@ -31098,145 +31751,612 @@ var init_mcp2 = __esm(() => {
31098
31751
  });
31099
31752
  server.registerTool("mark_read", {
31100
31753
  title: "Mark Read",
31101
- description: "Mark message IDs as read for the current agent.",
31754
+ description: "Mark message IDs as read for the current agent. Set 'all' to true to mark all unread messages as read.",
31102
31755
  inputSchema: {
31103
- ids: exports_external.array(exports_external.number()).describe("Message IDs to mark as read")
31756
+ ids: exports_external.array(exports_external.number()).optional().describe("Message IDs to mark as read"),
31757
+ all: exports_external.boolean().optional().describe("Mark all unread messages as read")
31104
31758
  }
31105
- }, async ({ ids }) => {
31759
+ }, async ({ ids, all }) => {
31106
31760
  const agent = resolveIdentity();
31107
- const count = markRead(ids, agent);
31761
+ let count;
31762
+ if (all) {
31763
+ count = markAllRead(agent);
31764
+ } else if (ids && ids.length > 0) {
31765
+ count = markRead(ids, agent);
31766
+ } else {
31767
+ return {
31768
+ content: [{ type: "text", text: "Provide message IDs or set 'all' to true." }],
31769
+ isError: true
31770
+ };
31771
+ }
31108
31772
  return {
31109
31773
  content: [{ type: "text", text: JSON.stringify({ marked_read: count }, null, 2) }]
31110
31774
  };
31111
31775
  });
31112
- server.registerTool("create_channel", {
31113
- title: "Create Channel",
31114
- description: "Create a new channel. The creator is auto-joined.",
31776
+ server.registerTool("search_messages", {
31777
+ title: "Search Messages",
31778
+ description: "Full-text search across message content. Returns matching messages ordered by newest first.",
31779
+ inputSchema: {
31780
+ query: exports_external.string().describe("Search query string"),
31781
+ space: exports_external.string().optional().describe("Filter by space name"),
31782
+ from: exports_external.string().optional().describe("Filter by sender agent ID"),
31783
+ to: exports_external.string().optional().describe("Filter by recipient agent ID"),
31784
+ limit: exports_external.number().optional().describe("Max results to return (default 50)")
31785
+ }
31786
+ }, async ({ query, space, from, to, limit }) => {
31787
+ const messages = searchMessages({ query, space, from, to, limit });
31788
+ return {
31789
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
31790
+ };
31791
+ });
31792
+ server.registerTool("export_messages", {
31793
+ title: "Export Messages",
31794
+ description: "Export messages as JSON or CSV with optional filters.",
31795
+ inputSchema: {
31796
+ space: exports_external.string().optional().describe("Filter by space name"),
31797
+ session_id: exports_external.string().optional().describe("Filter by session ID"),
31798
+ from: exports_external.string().optional().describe("Filter by sender agent ID"),
31799
+ since: exports_external.string().optional().describe("Messages after this ISO date"),
31800
+ until: exports_external.string().optional().describe("Messages before this ISO date"),
31801
+ format: exports_external.enum(["json", "csv"]).optional().describe("Output format (default: json)")
31802
+ }
31803
+ }, async ({ space, session_id, from, since, until, format }) => {
31804
+ const result = exportMessages({ space, session_id, from, since, until, format });
31805
+ return {
31806
+ content: [{ type: "text", text: result }]
31807
+ };
31808
+ });
31809
+ server.registerTool("create_space", {
31810
+ title: "Create Space",
31811
+ description: "Create a new space. The creator is auto-joined. Spaces can be nested (max 3 levels) and associated with a project.",
31115
31812
  inputSchema: {
31116
- name: exports_external.string().describe("Channel name (e.g. 'deployments', 'code-review')"),
31117
- description: exports_external.string().optional().describe("Channel description")
31813
+ name: exports_external.string().describe("Space name (e.g. 'deployments', 'code-review')"),
31814
+ description: exports_external.string().optional().describe("Space description"),
31815
+ parent_id: exports_external.string().optional().describe("Parent space name for nesting (max 3 levels deep)"),
31816
+ project_id: exports_external.string().optional().describe("Project ID to associate this space with")
31118
31817
  }
31119
- }, async ({ name, description }) => {
31818
+ }, async ({ name, description, parent_id, project_id }) => {
31120
31819
  const agent = resolveIdentity();
31121
31820
  try {
31122
- const ch = createChannel(name, agent, description);
31821
+ const sp = createSpace(name, agent, { description, parent_id, project_id });
31123
31822
  return {
31124
- content: [{ type: "text", text: JSON.stringify(ch, null, 2) }]
31823
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
31125
31824
  };
31126
31825
  } catch (e) {
31127
31826
  if (e.message?.includes("UNIQUE constraint")) {
31128
31827
  return {
31129
- content: [{ type: "text", text: `Channel #${name} already exists` }],
31828
+ content: [{ type: "text", text: `Space #${name} already exists` }],
31130
31829
  isError: true
31131
31830
  };
31132
31831
  }
31133
- throw e;
31832
+ return {
31833
+ content: [{ type: "text", text: e.message }],
31834
+ isError: true
31835
+ };
31134
31836
  }
31135
31837
  });
31136
- server.registerTool("list_channels", {
31137
- title: "List Channels",
31138
- description: "List all available channels with member and message counts."
31139
- }, async () => {
31140
- const channels = listChannels();
31838
+ server.registerTool("list_spaces", {
31839
+ title: "List Spaces",
31840
+ description: "List all available spaces with member and message counts. Can filter by project or parent. Archived spaces are excluded by default.",
31841
+ inputSchema: {
31842
+ project_id: exports_external.string().optional().describe("Filter by project ID"),
31843
+ parent_id: exports_external.string().optional().describe("Filter by parent space name. Use 'null' for top-level only."),
31844
+ include_archived: exports_external.boolean().optional().describe("Include archived spaces (default: false)")
31845
+ }
31846
+ }, async ({ project_id, parent_id, include_archived }) => {
31847
+ const opts = {};
31848
+ if (project_id)
31849
+ opts.project_id = project_id;
31850
+ if (parent_id === "null") {
31851
+ opts.parent_id = null;
31852
+ } else if (parent_id) {
31853
+ opts.parent_id = parent_id;
31854
+ }
31855
+ if (include_archived)
31856
+ opts.include_archived = true;
31857
+ const spaces = listSpaces(opts);
31141
31858
  return {
31142
- content: [{ type: "text", text: JSON.stringify(channels, null, 2) }]
31859
+ content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }]
31143
31860
  };
31144
31861
  });
31145
- server.registerTool("send_to_channel", {
31146
- title: "Send to Channel",
31147
- description: "Send a message to a channel. All members can see it.",
31862
+ server.registerTool("send_to_space", {
31863
+ title: "Send to Space",
31864
+ description: "Send a message to a space. All members can see it.",
31148
31865
  inputSchema: {
31149
- channel: exports_external.string().describe("Channel name"),
31866
+ space: exports_external.string().describe("Space name"),
31150
31867
  content: exports_external.string().describe("Message content"),
31151
31868
  priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional().describe("Message priority")
31152
31869
  }
31153
- }, async ({ channel, content, priority }) => {
31870
+ }, async ({ space, content, priority }) => {
31154
31871
  const from = resolveIdentity();
31155
- const ch = getChannel(channel);
31156
- if (!ch) {
31872
+ const sp = getSpace(space);
31873
+ if (!sp) {
31157
31874
  return {
31158
- content: [{ type: "text", text: `Channel #${channel} not found` }],
31875
+ content: [{ type: "text", text: `Space #${space} not found` }],
31159
31876
  isError: true
31160
31877
  };
31161
31878
  }
31162
31879
  const msg = sendMessage({
31163
31880
  from,
31164
- to: channel,
31881
+ to: space,
31165
31882
  content,
31166
- channel,
31167
- session_id: `channel:${channel}`,
31883
+ space,
31884
+ session_id: `space:${space}`,
31168
31885
  priority
31169
31886
  });
31170
31887
  return {
31171
31888
  content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
31172
31889
  };
31173
31890
  });
31174
- server.registerTool("read_channel", {
31175
- title: "Read Channel",
31176
- description: "Read messages from a channel.",
31891
+ server.registerTool("read_space", {
31892
+ title: "Read Space",
31893
+ description: "Read messages from a space.",
31177
31894
  inputSchema: {
31178
- channel: exports_external.string().describe("Channel name"),
31895
+ space: exports_external.string().describe("Space name"),
31179
31896
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
31180
31897
  limit: exports_external.number().optional().describe("Max messages to return")
31181
31898
  }
31182
- }, async ({ channel, since, limit }) => {
31183
- const messages = readMessages({ channel, since, limit });
31899
+ }, async ({ space, since, limit }) => {
31900
+ const messages = readMessages({ space, since, limit });
31184
31901
  return {
31185
31902
  content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
31186
31903
  };
31187
31904
  });
31188
- server.registerTool("join_channel", {
31189
- title: "Join Channel",
31190
- description: "Join a channel to receive messages.",
31905
+ server.registerTool("join_space", {
31906
+ title: "Join Space",
31907
+ description: "Join a space to receive messages.",
31191
31908
  inputSchema: {
31192
- channel: exports_external.string().describe("Channel name to join")
31909
+ space: exports_external.string().describe("Space name to join")
31193
31910
  }
31194
- }, async ({ channel }) => {
31911
+ }, async ({ space }) => {
31195
31912
  const agent = resolveIdentity();
31196
- const ok = joinChannel(channel, agent);
31913
+ const ok = joinSpace(space, agent);
31197
31914
  if (!ok) {
31198
31915
  return {
31199
- content: [{ type: "text", text: `Channel #${channel} not found` }],
31916
+ content: [{ type: "text", text: `Space #${space} not found` }],
31200
31917
  isError: true
31201
31918
  };
31202
31919
  }
31203
31920
  return {
31204
- content: [{ type: "text", text: JSON.stringify({ channel, agent, joined: true }, null, 2) }]
31921
+ content: [{ type: "text", text: JSON.stringify({ space, agent, joined: true }, null, 2) }]
31205
31922
  };
31206
31923
  });
31207
- server.registerTool("leave_channel", {
31208
- title: "Leave Channel",
31209
- description: "Leave a channel.",
31924
+ server.registerTool("leave_space", {
31925
+ title: "Leave Space",
31926
+ description: "Leave a space.",
31210
31927
  inputSchema: {
31211
- channel: exports_external.string().describe("Channel name to leave")
31928
+ space: exports_external.string().describe("Space name to leave")
31212
31929
  }
31213
- }, async ({ channel }) => {
31930
+ }, async ({ space }) => {
31214
31931
  const agent = resolveIdentity();
31215
- const left = leaveChannel(channel, agent);
31932
+ const left = leaveSpace(space, agent);
31216
31933
  return {
31217
- content: [{ type: "text", text: JSON.stringify({ channel, agent, left }, null, 2) }]
31934
+ content: [{ type: "text", text: JSON.stringify({ space, agent, left }, null, 2) }]
31218
31935
  };
31219
31936
  });
31220
- isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
31221
- if (isDirectRun) {
31222
- startMcpServer().catch((error48) => {
31223
- console.error("MCP server error:", error48);
31224
- process.exit(1);
31225
- });
31226
- }
31227
- });
31228
-
31229
- // src/server/serve.ts
31230
- var exports_serve = {};
31231
- __export(exports_serve, {
31232
- startDashboardServer: () => startDashboardServer
31937
+ server.registerTool("update_space", {
31938
+ title: "Update Space",
31939
+ description: "Update a space's description, parent, or project association.",
31940
+ inputSchema: {
31941
+ name: exports_external.string().describe("Space name to update"),
31942
+ description: exports_external.string().optional().describe("New description"),
31943
+ parent_id: exports_external.string().optional().describe("New parent space name (use 'null' to remove parent)"),
31944
+ project_id: exports_external.string().optional().describe("New project ID (use 'null' to remove project)")
31945
+ }
31946
+ }, async ({ name, description, parent_id, project_id }) => {
31947
+ const updates = {};
31948
+ if (description !== undefined)
31949
+ updates.description = description;
31950
+ if (parent_id !== undefined)
31951
+ updates.parent_id = parent_id === "null" ? null : parent_id;
31952
+ if (project_id !== undefined)
31953
+ updates.project_id = project_id === "null" ? null : project_id;
31954
+ try {
31955
+ const sp = updateSpace(name, updates);
31956
+ return {
31957
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
31958
+ };
31959
+ } catch (e) {
31960
+ return {
31961
+ content: [{ type: "text", text: e.message }],
31962
+ isError: true
31963
+ };
31964
+ }
31965
+ });
31966
+ server.registerTool("archive_space", {
31967
+ title: "Archive Space",
31968
+ description: "Archive a space. Archived spaces are hidden from list by default.",
31969
+ inputSchema: {
31970
+ name: exports_external.string().describe("Space name to archive")
31971
+ }
31972
+ }, async ({ name }) => {
31973
+ try {
31974
+ const sp = archiveSpace(name);
31975
+ return {
31976
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
31977
+ };
31978
+ } catch (e) {
31979
+ return {
31980
+ content: [{ type: "text", text: e.message }],
31981
+ isError: true
31982
+ };
31983
+ }
31984
+ });
31985
+ server.registerTool("unarchive_space", {
31986
+ title: "Unarchive Space",
31987
+ description: "Unarchive a previously archived space.",
31988
+ inputSchema: {
31989
+ name: exports_external.string().describe("Space name to unarchive")
31990
+ }
31991
+ }, async ({ name }) => {
31992
+ try {
31993
+ const sp = unarchiveSpace(name);
31994
+ return {
31995
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
31996
+ };
31997
+ } catch (e) {
31998
+ return {
31999
+ content: [{ type: "text", text: e.message }],
32000
+ isError: true
32001
+ };
32002
+ }
32003
+ });
32004
+ server.registerTool("create_project", {
32005
+ title: "Create Project",
32006
+ description: "Create a new project. Projects organize spaces and provide context for agent collaboration.",
32007
+ inputSchema: {
32008
+ name: exports_external.string().describe("Project name (unique)"),
32009
+ description: exports_external.string().optional().describe("Project description"),
32010
+ path: exports_external.string().optional().describe("Absolute path to project on disk"),
32011
+ repository: exports_external.string().optional().describe("Repository URL"),
32012
+ tags: exports_external.string().optional().describe(`JSON array of tags (e.g. '["backend", "api"]')`),
32013
+ metadata: exports_external.string().optional().describe("JSON metadata string"),
32014
+ settings: exports_external.string().optional().describe("JSON settings string")
32015
+ }
32016
+ }, async ({ name, description, path, repository, tags, metadata, settings }) => {
32017
+ const agent = resolveIdentity();
32018
+ let parsedTags;
32019
+ if (tags) {
32020
+ try {
32021
+ parsedTags = JSON.parse(tags);
32022
+ } catch {
32023
+ return {
32024
+ content: [{ type: "text", text: "Invalid tags JSON. Expected array of strings." }],
32025
+ isError: true
32026
+ };
32027
+ }
32028
+ }
32029
+ let parsedMetadata;
32030
+ if (metadata) {
32031
+ try {
32032
+ parsedMetadata = JSON.parse(metadata);
32033
+ } catch {
32034
+ return {
32035
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
32036
+ isError: true
32037
+ };
32038
+ }
32039
+ }
32040
+ let parsedSettings;
32041
+ if (settings) {
32042
+ try {
32043
+ parsedSettings = JSON.parse(settings);
32044
+ } catch {
32045
+ return {
32046
+ content: [{ type: "text", text: "Invalid settings JSON." }],
32047
+ isError: true
32048
+ };
32049
+ }
32050
+ }
32051
+ try {
32052
+ const project = createProject({
32053
+ name,
32054
+ created_by: agent,
32055
+ description,
32056
+ path,
32057
+ repository,
32058
+ tags: parsedTags,
32059
+ metadata: parsedMetadata,
32060
+ settings: parsedSettings
32061
+ });
32062
+ return {
32063
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
32064
+ };
32065
+ } catch (e) {
32066
+ if (e.message?.includes("UNIQUE constraint")) {
32067
+ return {
32068
+ content: [{ type: "text", text: `Project "${name}" already exists` }],
32069
+ isError: true
32070
+ };
32071
+ }
32072
+ return {
32073
+ content: [{ type: "text", text: e.message }],
32074
+ isError: true
32075
+ };
32076
+ }
32077
+ });
32078
+ server.registerTool("list_projects", {
32079
+ title: "List Projects",
32080
+ description: "List all registered projects.",
32081
+ inputSchema: {
32082
+ status: exports_external.enum(["active", "archived"]).optional().describe("Filter by project status")
32083
+ }
32084
+ }, async ({ status }) => {
32085
+ const projects = listProjects(status ? { status } : undefined);
32086
+ return {
32087
+ content: [{ type: "text", text: JSON.stringify(projects, null, 2) }]
32088
+ };
32089
+ });
32090
+ server.registerTool("get_project", {
32091
+ title: "Get Project",
32092
+ description: "Get full details of a project by ID or name.",
32093
+ inputSchema: {
32094
+ id: exports_external.string().describe("Project ID (UUID) or name")
32095
+ }
32096
+ }, async ({ id }) => {
32097
+ let project = getProject(id);
32098
+ if (!project) {
32099
+ project = getProjectByName(id);
32100
+ }
32101
+ if (!project) {
32102
+ return {
32103
+ content: [{ type: "text", text: `Project "${id}" not found` }],
32104
+ isError: true
32105
+ };
32106
+ }
32107
+ return {
32108
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
32109
+ };
32110
+ });
32111
+ server.registerTool("update_project", {
32112
+ title: "Update Project",
32113
+ description: "Update a project's fields.",
32114
+ inputSchema: {
32115
+ id: exports_external.string().describe("Project ID (UUID)"),
32116
+ name: exports_external.string().optional().describe("New project name"),
32117
+ description: exports_external.string().optional().describe("New description"),
32118
+ path: exports_external.string().optional().describe("New path"),
32119
+ status: exports_external.enum(["active", "archived"]).optional().describe("New status"),
32120
+ repository: exports_external.string().optional().describe("New repository URL"),
32121
+ tags: exports_external.string().optional().describe("JSON array of tags"),
32122
+ metadata: exports_external.string().optional().describe("JSON metadata string"),
32123
+ settings: exports_external.string().optional().describe("JSON settings string")
32124
+ }
32125
+ }, async ({ id, name, description, path, status, repository, tags, metadata, settings }) => {
32126
+ const updates = {};
32127
+ if (name !== undefined)
32128
+ updates.name = name;
32129
+ if (description !== undefined)
32130
+ updates.description = description;
32131
+ if (path !== undefined)
32132
+ updates.path = path;
32133
+ if (status !== undefined)
32134
+ updates.status = status;
32135
+ if (repository !== undefined)
32136
+ updates.repository = repository;
32137
+ if (tags) {
32138
+ try {
32139
+ updates.tags = JSON.parse(tags);
32140
+ } catch {
32141
+ return {
32142
+ content: [{ type: "text", text: "Invalid tags JSON." }],
32143
+ isError: true
32144
+ };
32145
+ }
32146
+ }
32147
+ if (metadata) {
32148
+ try {
32149
+ updates.metadata = JSON.parse(metadata);
32150
+ } catch {
32151
+ return {
32152
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
32153
+ isError: true
32154
+ };
32155
+ }
32156
+ }
32157
+ if (settings) {
32158
+ try {
32159
+ updates.settings = JSON.parse(settings);
32160
+ } catch {
32161
+ return {
32162
+ content: [{ type: "text", text: "Invalid settings JSON." }],
32163
+ isError: true
32164
+ };
32165
+ }
32166
+ }
32167
+ try {
32168
+ const project = updateProject(id, updates);
32169
+ return {
32170
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
32171
+ };
32172
+ } catch (e) {
32173
+ return {
32174
+ content: [{ type: "text", text: e.message }],
32175
+ isError: true
32176
+ };
32177
+ }
32178
+ });
32179
+ server.registerTool("delete_project", {
32180
+ title: "Delete Project",
32181
+ description: "Delete a project permanently. Fails if spaces still reference it.",
32182
+ inputSchema: {
32183
+ id: exports_external.string().describe("Project ID (UUID)")
32184
+ }
32185
+ }, async ({ id }) => {
32186
+ try {
32187
+ const deleted = deleteProject(id);
32188
+ if (!deleted) {
32189
+ return {
32190
+ content: [{ type: "text", text: `Project "${id}" not found` }],
32191
+ isError: true
32192
+ };
32193
+ }
32194
+ return {
32195
+ content: [{ type: "text", text: JSON.stringify({ id, deleted: true }, null, 2) }]
32196
+ };
32197
+ } catch (e) {
32198
+ return {
32199
+ content: [{ type: "text", text: e.message }],
32200
+ isError: true
32201
+ };
32202
+ }
32203
+ });
32204
+ server.registerTool("delete_message", {
32205
+ title: "Delete Message",
32206
+ description: "Delete a message. Only the sender can delete their own messages. The agent is auto-resolved.",
32207
+ inputSchema: {
32208
+ id: exports_external.number().describe("Message ID to delete")
32209
+ }
32210
+ }, async ({ id }) => {
32211
+ const agent = resolveIdentity();
32212
+ const deleted = deleteMessage(id, agent);
32213
+ if (!deleted) {
32214
+ return {
32215
+ content: [{ type: "text", text: `Message #${id} not found or not your message` }],
32216
+ isError: true
32217
+ };
32218
+ }
32219
+ return {
32220
+ content: [{ type: "text", text: JSON.stringify({ deleted: true }, null, 2) }]
32221
+ };
32222
+ });
32223
+ server.registerTool("edit_message", {
32224
+ title: "Edit Message",
32225
+ description: "Edit a message's content. Only the sender can edit their own messages. The agent is auto-resolved.",
32226
+ inputSchema: {
32227
+ id: exports_external.number().describe("Message ID to edit"),
32228
+ content: exports_external.string().describe("New message content")
32229
+ }
32230
+ }, async ({ id, content }) => {
32231
+ const agent = resolveIdentity();
32232
+ const msg = editMessage(id, agent, content);
32233
+ if (!msg) {
32234
+ return {
32235
+ content: [{ type: "text", text: `Message #${id} not found or not your message` }],
32236
+ isError: true
32237
+ };
32238
+ }
32239
+ return {
32240
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
32241
+ };
32242
+ });
32243
+ server.registerTool("pin_message", {
32244
+ title: "Pin Message",
32245
+ description: "Pin a message. Pinned messages can be retrieved with get_pinned_messages.",
32246
+ inputSchema: {
32247
+ id: exports_external.number().describe("Message ID to pin")
32248
+ }
32249
+ }, async ({ id }) => {
32250
+ const msg = pinMessage(id);
32251
+ if (!msg) {
32252
+ return {
32253
+ content: [{ type: "text", text: `Message #${id} not found` }],
32254
+ isError: true
32255
+ };
32256
+ }
32257
+ return {
32258
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
32259
+ };
32260
+ });
32261
+ server.registerTool("unpin_message", {
32262
+ title: "Unpin Message",
32263
+ description: "Unpin a previously pinned message.",
32264
+ inputSchema: {
32265
+ id: exports_external.number().describe("Message ID to unpin")
32266
+ }
32267
+ }, async ({ id }) => {
32268
+ const msg = unpinMessage(id);
32269
+ if (!msg) {
32270
+ return {
32271
+ content: [{ type: "text", text: `Message #${id} not found` }],
32272
+ isError: true
32273
+ };
32274
+ }
32275
+ return {
32276
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
32277
+ };
32278
+ });
32279
+ server.registerTool("get_pinned_messages", {
32280
+ title: "Get Pinned Messages",
32281
+ description: "Retrieve pinned messages, optionally filtered by space or session.",
32282
+ inputSchema: {
32283
+ space: exports_external.string().optional().describe("Filter by space name"),
32284
+ session_id: exports_external.string().optional().describe("Filter by session ID"),
32285
+ limit: exports_external.number().optional().describe("Max messages to return")
32286
+ }
32287
+ }, async ({ space, session_id, limit }) => {
32288
+ const messages = getPinnedMessages({ space, session_id, limit });
32289
+ return {
32290
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
32291
+ };
32292
+ });
32293
+ server.registerTool("heartbeat", {
32294
+ title: "Heartbeat",
32295
+ description: "Send a heartbeat to indicate agent is alive. Auto-resolves agent from CONVERSATIONS_AGENT_ID env var. Optionally set a status.",
32296
+ inputSchema: {
32297
+ status: exports_external.string().optional().describe("Agent status (e.g. 'online', 'busy', 'idle'). Defaults to 'online'.")
32298
+ }
32299
+ }, async ({ status }) => {
32300
+ const agent = resolveIdentity();
32301
+ heartbeat(agent, status);
32302
+ return {
32303
+ content: [{ type: "text", text: JSON.stringify({ agent, status: status || "online", heartbeat: true }, null, 2) }]
32304
+ };
32305
+ });
32306
+ server.registerTool("list_agents", {
32307
+ title: "List Agents",
32308
+ description: "List all agents with their presence status. Returns agent name, status, last seen time, and whether they are online.",
32309
+ inputSchema: {
32310
+ online_only: exports_external.boolean().optional().describe("Only return agents that are currently online (seen within last 60 seconds)")
32311
+ }
32312
+ }, async ({ online_only }) => {
32313
+ const agents = listAgents({ online_only });
32314
+ return {
32315
+ content: [{ type: "text", text: JSON.stringify(agents, null, 2) }]
32316
+ };
32317
+ });
32318
+ isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
32319
+ if (isDirectRun) {
32320
+ startMcpServer().catch((error48) => {
32321
+ console.error("MCP server error:", error48);
32322
+ process.exit(1);
32323
+ });
32324
+ }
32325
+ });
32326
+
32327
+ // src/server/serve.ts
32328
+ var exports_serve = {};
32329
+ __export(exports_serve, {
32330
+ startDashboardServer: () => startDashboardServer
31233
32331
  });
31234
- import { join as join2 } from "path";
32332
+ import { join as join2, resolve, sep } from "path";
31235
32333
  import { existsSync } from "fs";
32334
+ function securityHeaders(base) {
32335
+ const headers = new Headers(base);
32336
+ if (!headers.has("X-Content-Type-Options"))
32337
+ headers.set("X-Content-Type-Options", "nosniff");
32338
+ if (!headers.has("X-Frame-Options"))
32339
+ headers.set("X-Frame-Options", "DENY");
32340
+ if (!headers.has("Referrer-Policy"))
32341
+ headers.set("Referrer-Policy", "no-referrer");
32342
+ if (!headers.has("Permissions-Policy")) {
32343
+ headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
32344
+ }
32345
+ if (!headers.has("Cross-Origin-Resource-Policy")) {
32346
+ headers.set("Cross-Origin-Resource-Policy", "same-origin");
32347
+ }
32348
+ if (!headers.has("Content-Security-Policy")) {
32349
+ headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'");
32350
+ }
32351
+ return headers;
32352
+ }
31236
32353
  function jsonResponse(data, status = 200) {
31237
32354
  return new Response(JSON.stringify(data), {
31238
32355
  status,
31239
- headers: { "Content-Type": "application/json" }
32356
+ headers: securityHeaders({
32357
+ "Content-Type": "application/json; charset=utf-8",
32358
+ "Cache-Control": "no-store"
32359
+ })
31240
32360
  });
31241
32361
  }
31242
32362
  function getStatus() {
@@ -31245,20 +32365,46 @@ function getStatus() {
31245
32365
  const totalMessages = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
31246
32366
  const totalSessions = db2.prepare("SELECT COUNT(DISTINCT session_id) as count FROM messages").get().count;
31247
32367
  const totalUnread = db2.prepare("SELECT COUNT(*) as count FROM messages WHERE read_at IS NULL").get().count;
31248
- const totalChannels = db2.prepare("SELECT COUNT(*) as count FROM channels").get().count;
32368
+ const totalSpaces = db2.prepare("SELECT COUNT(*) as count FROM spaces").get().count;
32369
+ const totalProjects = db2.prepare("SELECT COUNT(*) as count FROM projects").get().count;
31249
32370
  return {
31250
32371
  db_path: dbPath,
31251
32372
  total_messages: totalMessages,
31252
32373
  total_sessions: totalSessions,
31253
- total_channels: totalChannels,
32374
+ total_spaces: totalSpaces,
32375
+ total_projects: totalProjects,
31254
32376
  unread_messages: totalUnread
31255
32377
  };
31256
32378
  }
31257
- function startDashboardServer(port = 3456) {
32379
+ function normalizeHost(value) {
32380
+ const host = typeof value === "string" ? value.trim() : "";
32381
+ return host.length > 0 ? host : "127.0.0.1";
32382
+ }
32383
+ function normalizePort(value, fallback) {
32384
+ const parsed = typeof value === "string" ? parseInt(value, 10) : value;
32385
+ if (!Number.isFinite(parsed))
32386
+ return fallback;
32387
+ const port = parsed;
32388
+ if (port < 0 || port > 65535)
32389
+ return fallback;
32390
+ return port;
32391
+ }
32392
+ function isSameOrigin(req) {
32393
+ const origin = req.headers.get("origin");
32394
+ if (!origin)
32395
+ return true;
32396
+ if (origin === "null")
32397
+ return false;
32398
+ return origin === new URL(req.url).origin;
32399
+ }
32400
+ function startDashboardServer(port = 3456, host) {
32401
+ const resolvedPort = normalizePort(port, 3456);
32402
+ const resolvedHost = normalizeHost(host ?? process.env.CONVERSATIONS_DASHBOARD_HOST);
31258
32403
  const dashboardDist = join2(import.meta.dir, "../../dashboard/dist");
31259
32404
  const hasDist = existsSync(dashboardDist);
31260
32405
  const server2 = Bun.serve({
31261
- port,
32406
+ port: resolvedPort,
32407
+ hostname: resolvedHost,
31262
32408
  async fetch(req) {
31263
32409
  const url2 = new URL(req.url);
31264
32410
  const path = url2.pathname;
@@ -31266,57 +32412,390 @@ function startDashboardServer(port = 3456) {
31266
32412
  return jsonResponse(getStatus());
31267
32413
  }
31268
32414
  if (path === "/api/messages" && req.method === "GET") {
31269
- const limit = parseInt(url2.searchParams.get("limit") || "50");
32415
+ const rawLimit = url2.searchParams.get("limit");
32416
+ let limit = parseInt(rawLimit || "50", 10);
32417
+ if (!Number.isFinite(limit) || limit <= 0)
32418
+ limit = 50;
32419
+ if (limit > 500)
32420
+ limit = 500;
31270
32421
  const session = url2.searchParams.get("session") || undefined;
31271
- const channel = url2.searchParams.get("channel") || undefined;
32422
+ const space = url2.searchParams.get("space") || undefined;
31272
32423
  const from = url2.searchParams.get("from") || undefined;
31273
32424
  const to = url2.searchParams.get("to") || undefined;
31274
- const messages = readMessages({ session_id: session, channel, from, to, limit });
31275
- return jsonResponse(messages.reverse());
32425
+ const messages = readMessages({ session_id: session, space, from, to, limit, order: "desc" });
32426
+ return jsonResponse(messages);
31276
32427
  }
31277
32428
  if (path === "/api/messages" && req.method === "POST") {
32429
+ if (!isSameOrigin(req)) {
32430
+ return jsonResponse({ error: "Invalid origin" }, 403);
32431
+ }
31278
32432
  try {
31279
32433
  const text = await req.text();
31280
32434
  const body = JSON.parse(text);
32435
+ const from = typeof body.from === "string" ? body.from.trim() : "";
32436
+ const to = typeof body.to === "string" ? body.to.trim() : "";
32437
+ const content = typeof body.content === "string" ? body.content.trim() : "";
32438
+ const space = typeof body.space === "string" ? body.space.trim() : undefined;
32439
+ const priority = typeof body.priority === "string" ? body.priority.trim().toLowerCase() : undefined;
32440
+ if (!from || !to || !content) {
32441
+ return jsonResponse({ error: "from, to, and content are required" }, 400);
32442
+ }
32443
+ if (priority && !["low", "normal", "high", "urgent"].includes(priority)) {
32444
+ return jsonResponse({ error: "Invalid priority" }, 400);
32445
+ }
31281
32446
  const msg = sendMessage({
31282
- from: body.from,
31283
- to: body.to,
31284
- content: body.content,
31285
- channel: body.channel,
31286
- priority: body.priority
32447
+ from,
32448
+ to,
32449
+ content,
32450
+ space,
32451
+ priority
31287
32452
  });
31288
32453
  return jsonResponse(msg);
31289
32454
  } catch (e) {
31290
32455
  return jsonResponse({ error: e.message }, 400);
31291
32456
  }
31292
32457
  }
32458
+ if (path === "/api/messages/search" && req.method === "GET") {
32459
+ const q = url2.searchParams.get("q") || "";
32460
+ if (!q.trim()) {
32461
+ return jsonResponse({ error: "Query parameter 'q' is required" }, 400);
32462
+ }
32463
+ const rawLimit = url2.searchParams.get("limit");
32464
+ let limit = parseInt(rawLimit || "50", 10);
32465
+ if (!Number.isFinite(limit) || limit <= 0)
32466
+ limit = 50;
32467
+ if (limit > 500)
32468
+ limit = 500;
32469
+ const space = url2.searchParams.get("space") || undefined;
32470
+ const from = url2.searchParams.get("from") || undefined;
32471
+ const to = url2.searchParams.get("to") || undefined;
32472
+ const messages = searchMessages({ query: q.trim(), space, from, to, limit });
32473
+ return jsonResponse(messages);
32474
+ }
32475
+ if (path === "/api/export" && req.method === "GET") {
32476
+ const space = url2.searchParams.get("space") || undefined;
32477
+ const session = url2.searchParams.get("session") || undefined;
32478
+ const from = url2.searchParams.get("from") || undefined;
32479
+ const since = url2.searchParams.get("since") || undefined;
32480
+ const until = url2.searchParams.get("until") || undefined;
32481
+ const format = url2.searchParams.get("format") === "csv" ? "csv" : "json";
32482
+ const result = exportMessages({ space, session_id: session, from, since, until, format });
32483
+ if (format === "csv") {
32484
+ return new Response(result, {
32485
+ status: 200,
32486
+ headers: securityHeaders({
32487
+ "Content-Type": "text/csv; charset=utf-8",
32488
+ "Content-Disposition": 'attachment; filename="messages.csv"',
32489
+ "Cache-Control": "no-store"
32490
+ })
32491
+ });
32492
+ }
32493
+ return jsonResponse(JSON.parse(result));
32494
+ }
32495
+ if (path === "/api/messages/pinned" && req.method === "GET") {
32496
+ const space = url2.searchParams.get("space") || undefined;
32497
+ const session_id = url2.searchParams.get("session_id") || undefined;
32498
+ const rawLimit = url2.searchParams.get("limit");
32499
+ let limit;
32500
+ if (rawLimit) {
32501
+ limit = parseInt(rawLimit, 10);
32502
+ if (!Number.isFinite(limit) || limit <= 0)
32503
+ limit = 50;
32504
+ if (limit > 500)
32505
+ limit = 500;
32506
+ }
32507
+ const messages = getPinnedMessages({ space, session_id, limit });
32508
+ return jsonResponse(messages);
32509
+ }
32510
+ const pinMatch = path.match(/^\/api\/messages\/(\d+)\/pin$/);
32511
+ if (pinMatch) {
32512
+ const messageId = parseInt(pinMatch[1], 10);
32513
+ if (req.method === "POST") {
32514
+ if (!isSameOrigin(req)) {
32515
+ return jsonResponse({ error: "Invalid origin" }, 403);
32516
+ }
32517
+ const msg = pinMessage(messageId);
32518
+ if (!msg)
32519
+ return jsonResponse({ error: "Message not found" }, 404);
32520
+ return jsonResponse(msg);
32521
+ }
32522
+ if (req.method === "DELETE") {
32523
+ if (!isSameOrigin(req)) {
32524
+ return jsonResponse({ error: "Invalid origin" }, 403);
32525
+ }
32526
+ const msg = unpinMessage(messageId);
32527
+ if (!msg)
32528
+ return jsonResponse({ error: "Message not found" }, 404);
32529
+ return jsonResponse(msg);
32530
+ }
32531
+ }
32532
+ const messageMatch = path.match(/^\/api\/messages\/(\d+)$/);
32533
+ if (messageMatch) {
32534
+ const messageId = parseInt(messageMatch[1], 10);
32535
+ if (req.method === "DELETE") {
32536
+ if (!isSameOrigin(req)) {
32537
+ return jsonResponse({ error: "Invalid origin" }, 403);
32538
+ }
32539
+ const from = url2.searchParams.get("from") || "";
32540
+ if (!from) {
32541
+ return jsonResponse({ error: "'from' query parameter is required" }, 400);
32542
+ }
32543
+ const deleted = deleteMessage(messageId, from);
32544
+ if (!deleted)
32545
+ return jsonResponse({ error: "Message not found or not your message" }, 404);
32546
+ return jsonResponse({ id: messageId, deleted: true });
32547
+ }
32548
+ if (req.method === "PUT") {
32549
+ if (!isSameOrigin(req)) {
32550
+ return jsonResponse({ error: "Invalid origin" }, 403);
32551
+ }
32552
+ try {
32553
+ const text = await req.text();
32554
+ const body = JSON.parse(text);
32555
+ const content = typeof body.content === "string" ? body.content.trim() : "";
32556
+ const from = typeof body.from === "string" ? body.from.trim() : "";
32557
+ if (!content || !from) {
32558
+ return jsonResponse({ error: "content and from are required" }, 400);
32559
+ }
32560
+ const msg = editMessage(messageId, from, content);
32561
+ if (!msg)
32562
+ return jsonResponse({ error: "Message not found or not your message" }, 404);
32563
+ return jsonResponse(msg);
32564
+ } catch (e) {
32565
+ return jsonResponse({ error: e.message }, 400);
32566
+ }
32567
+ }
32568
+ }
31293
32569
  if (path === "/api/sessions") {
31294
32570
  const agent = url2.searchParams.get("agent") || undefined;
31295
32571
  return jsonResponse(listSessions(agent));
31296
32572
  }
31297
- if (path === "/api/channels" && req.method === "GET") {
31298
- return jsonResponse(listChannels());
32573
+ if (path === "/api/spaces" && req.method === "GET") {
32574
+ const projectId = url2.searchParams.get("project_id") || undefined;
32575
+ const includeArchived = url2.searchParams.get("include_archived") === "true";
32576
+ const listOpts = {};
32577
+ if (projectId)
32578
+ listOpts.project_id = projectId;
32579
+ if (includeArchived)
32580
+ listOpts.include_archived = true;
32581
+ return jsonResponse(listSpaces(Object.keys(listOpts).length > 0 ? listOpts : undefined));
32582
+ }
32583
+ if (path === "/api/spaces" && req.method === "POST") {
32584
+ if (!isSameOrigin(req)) {
32585
+ return jsonResponse({ error: "Invalid origin" }, 403);
32586
+ }
32587
+ try {
32588
+ const text = await req.text();
32589
+ const body = JSON.parse(text);
32590
+ const name = typeof body.name === "string" ? body.name.trim() : "";
32591
+ const createdBy = typeof body.created_by === "string" ? body.created_by.trim() : "";
32592
+ const description = typeof body.description === "string" ? body.description.trim() : undefined;
32593
+ const parent_id = typeof body.parent_id === "string" ? body.parent_id.trim() : undefined;
32594
+ const project_id = typeof body.project_id === "string" ? body.project_id.trim() : undefined;
32595
+ if (!name || !createdBy) {
32596
+ return jsonResponse({ error: "name and created_by are required" }, 400);
32597
+ }
32598
+ const sp = createSpace(name, createdBy, { description, parent_id, project_id });
32599
+ return jsonResponse(sp);
32600
+ } catch (e) {
32601
+ return jsonResponse({ error: e.message }, 400);
32602
+ }
32603
+ }
32604
+ const spaceArchiveMatch = path.match(/^\/api\/spaces\/([^/]+)\/archive$/);
32605
+ if (spaceArchiveMatch && req.method === "POST") {
32606
+ if (!isSameOrigin(req)) {
32607
+ return jsonResponse({ error: "Invalid origin" }, 403);
32608
+ }
32609
+ try {
32610
+ const sp = archiveSpace(decodeURIComponent(spaceArchiveMatch[1]));
32611
+ return jsonResponse(sp);
32612
+ } catch (e) {
32613
+ return jsonResponse({ error: e.message }, 400);
32614
+ }
32615
+ }
32616
+ const spaceUnarchiveMatch = path.match(/^\/api\/spaces\/([^/]+)\/unarchive$/);
32617
+ if (spaceUnarchiveMatch && req.method === "POST") {
32618
+ if (!isSameOrigin(req)) {
32619
+ return jsonResponse({ error: "Invalid origin" }, 403);
32620
+ }
32621
+ try {
32622
+ const sp = unarchiveSpace(decodeURIComponent(spaceUnarchiveMatch[1]));
32623
+ return jsonResponse(sp);
32624
+ } catch (e) {
32625
+ return jsonResponse({ error: e.message }, 400);
32626
+ }
32627
+ }
32628
+ const spaceMatch = path.match(/^\/api\/spaces\/([^/]+)$/);
32629
+ if (spaceMatch) {
32630
+ const spaceName = decodeURIComponent(spaceMatch[1]);
32631
+ if (req.method === "GET") {
32632
+ const sp = getSpace(spaceName);
32633
+ if (!sp)
32634
+ return jsonResponse({ error: "Space not found" }, 404);
32635
+ return jsonResponse(sp);
32636
+ }
32637
+ if (req.method === "PUT") {
32638
+ if (!isSameOrigin(req)) {
32639
+ return jsonResponse({ error: "Invalid origin" }, 403);
32640
+ }
32641
+ try {
32642
+ const text = await req.text();
32643
+ const body = JSON.parse(text);
32644
+ const updates = {};
32645
+ if (body.description !== undefined)
32646
+ updates.description = body.description;
32647
+ if (body.parent_id !== undefined)
32648
+ updates.parent_id = body.parent_id;
32649
+ if (body.project_id !== undefined)
32650
+ updates.project_id = body.project_id;
32651
+ const sp = updateSpace(spaceName, updates);
32652
+ return jsonResponse(sp);
32653
+ } catch (e) {
32654
+ return jsonResponse({ error: e.message }, 400);
32655
+ }
32656
+ }
32657
+ }
32658
+ if (path === "/api/projects" && req.method === "GET") {
32659
+ const status = url2.searchParams.get("status");
32660
+ return jsonResponse(listProjects(status ? { status } : undefined));
31299
32661
  }
31300
- if (path === "/api/channels" && req.method === "POST") {
32662
+ if (path === "/api/projects" && req.method === "POST") {
32663
+ if (!isSameOrigin(req)) {
32664
+ return jsonResponse({ error: "Invalid origin" }, 403);
32665
+ }
31301
32666
  try {
31302
32667
  const text = await req.text();
31303
32668
  const body = JSON.parse(text);
31304
- const ch = createChannel(body.name, body.created_by, body.description);
31305
- return jsonResponse(ch);
32669
+ const name = typeof body.name === "string" ? body.name.trim() : "";
32670
+ const createdBy = typeof body.created_by === "string" ? body.created_by.trim() : "";
32671
+ if (!name || !createdBy) {
32672
+ return jsonResponse({ error: "name and created_by are required" }, 400);
32673
+ }
32674
+ const project = createProject({
32675
+ name,
32676
+ created_by: createdBy,
32677
+ description: body.description,
32678
+ path: body.path,
32679
+ repository: body.repository,
32680
+ tags: body.tags,
32681
+ metadata: body.metadata,
32682
+ settings: body.settings
32683
+ });
32684
+ return jsonResponse(project);
31306
32685
  } catch (e) {
31307
32686
  return jsonResponse({ error: e.message }, 400);
31308
32687
  }
31309
32688
  }
32689
+ const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
32690
+ if (projectMatch) {
32691
+ const projectId = projectMatch[1];
32692
+ if (req.method === "GET") {
32693
+ let project = getProject(projectId);
32694
+ if (!project)
32695
+ project = getProjectByName(projectId);
32696
+ if (!project)
32697
+ return jsonResponse({ error: "Project not found" }, 404);
32698
+ return jsonResponse(project);
32699
+ }
32700
+ if (req.method === "PUT") {
32701
+ if (!isSameOrigin(req)) {
32702
+ return jsonResponse({ error: "Invalid origin" }, 403);
32703
+ }
32704
+ try {
32705
+ const text = await req.text();
32706
+ const body = JSON.parse(text);
32707
+ const project = updateProject(projectId, body);
32708
+ return jsonResponse(project);
32709
+ } catch (e) {
32710
+ return jsonResponse({ error: e.message }, 400);
32711
+ }
32712
+ }
32713
+ if (req.method === "DELETE") {
32714
+ if (!isSameOrigin(req)) {
32715
+ return jsonResponse({ error: "Invalid origin" }, 403);
32716
+ }
32717
+ try {
32718
+ const deleted = deleteProject(projectId);
32719
+ if (!deleted)
32720
+ return jsonResponse({ error: "Project not found" }, 404);
32721
+ return jsonResponse({ id: projectId, deleted: true });
32722
+ } catch (e) {
32723
+ return jsonResponse({ error: e.message }, 400);
32724
+ }
32725
+ }
32726
+ }
32727
+ if (path === "/api/agents" && req.method === "GET") {
32728
+ const onlineOnly = url2.searchParams.get("online_only") === "true";
32729
+ const agents = listAgents({ online_only: onlineOnly });
32730
+ return jsonResponse(agents);
32731
+ }
32732
+ if (path === "/api/version" && req.method === "GET") {
32733
+ try {
32734
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
32735
+ const current = pkg.version;
32736
+ const res = await fetch("https://registry.npmjs.org/@hasna/conversations/latest");
32737
+ const data = await res.json();
32738
+ const latest = data.version;
32739
+ return jsonResponse({ current, latest, updateAvailable: current !== latest });
32740
+ } catch (e) {
32741
+ return jsonResponse({ error: e.message }, 500);
32742
+ }
32743
+ }
32744
+ if (path === "/api/update" && req.method === "POST") {
32745
+ if (!isSameOrigin(req)) {
32746
+ return jsonResponse({ error: "Invalid origin" }, 403);
32747
+ }
32748
+ try {
32749
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
32750
+ const current = pkg.version;
32751
+ const res = await fetch("https://registry.npmjs.org/@hasna/conversations/latest");
32752
+ const data = await res.json();
32753
+ const latest = data.version;
32754
+ if (current === latest) {
32755
+ return jsonResponse({ current, latest, status: "up-to-date" });
32756
+ }
32757
+ const proc = Bun.spawn(["bun", "install", "-g", `@hasna/conversations@${latest}`], {
32758
+ stdout: "pipe",
32759
+ stderr: "pipe"
32760
+ });
32761
+ const exitCode = await proc.exited;
32762
+ const stdout = await new Response(proc.stdout).text();
32763
+ const stderr = await new Response(proc.stderr).text();
32764
+ if (exitCode === 0) {
32765
+ return jsonResponse({ current, latest, status: "updated", stdout });
32766
+ } else {
32767
+ return jsonResponse({ current, latest, status: "failed", exitCode, stderr }, 500);
32768
+ }
32769
+ } catch (e) {
32770
+ return jsonResponse({ error: e.message }, 500);
32771
+ }
32772
+ }
31310
32773
  if (hasDist) {
31311
- let filePath = join2(dashboardDist, path === "/" ? "index.html" : path);
32774
+ const baseDir = resolve(dashboardDist);
32775
+ const safePath = path === "/" ? "index.html" : path.replace(/^\/+/, "");
32776
+ const filePath = resolve(baseDir, safePath);
32777
+ if (!filePath.startsWith(baseDir + sep)) {
32778
+ return new Response("Not Found", { status: 404 });
32779
+ }
31312
32780
  let file2 = Bun.file(filePath);
31313
- if (await file2.exists())
31314
- return new Response(file2);
32781
+ if (await file2.exists()) {
32782
+ const headers = securityHeaders();
32783
+ if (file2.type)
32784
+ headers.set("Content-Type", file2.type);
32785
+ return new Response(file2, { headers });
32786
+ }
31315
32787
  file2 = Bun.file(join2(dashboardDist, "index.html"));
31316
- if (await file2.exists())
31317
- return new Response(file2);
32788
+ if (await file2.exists()) {
32789
+ const headers = securityHeaders();
32790
+ if (file2.type)
32791
+ headers.set("Content-Type", file2.type);
32792
+ return new Response(file2, { headers });
32793
+ }
31318
32794
  }
31319
- return new Response("Not Found", { status: 404 });
32795
+ return new Response("Not Found", {
32796
+ status: 404,
32797
+ headers: securityHeaders({ "Content-Type": "text/plain; charset=utf-8" })
32798
+ });
31320
32799
  }
31321
32800
  });
31322
32801
  console.log(`Dashboard running at http://localhost:${server2.port}`);
@@ -31326,11 +32805,13 @@ var isDirectRun2;
31326
32805
  var init_serve = __esm(() => {
31327
32806
  init_messages();
31328
32807
  init_sessions();
31329
- init_channels();
32808
+ init_spaces();
32809
+ init_projects();
31330
32810
  init_db();
32811
+ init_presence();
31331
32812
  isDirectRun2 = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("serve.ts") || process.argv[1]?.endsWith("serve.js");
31332
32813
  if (isDirectRun2) {
31333
- const port = parseInt(process.env.PORT || "3456");
32814
+ const port = normalizePort(process.env.PORT, 3456);
31334
32815
  startDashboardServer(port);
31335
32816
  }
31336
32817
  });
@@ -31354,8 +32835,10 @@ var {
31354
32835
  // src/cli/index.tsx
31355
32836
  init_messages();
31356
32837
  init_sessions();
31357
- init_channels();
32838
+ init_spaces();
32839
+ init_projects();
31358
32840
  init_db();
32841
+ init_presence();
31359
32842
  import chalk2 from "chalk";
31360
32843
  import { render } from "ink";
31361
32844
  import React8 from "react";
@@ -31865,15 +33348,21 @@ function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicator
31865
33348
  var SelectInput_default = SelectInput;
31866
33349
  // src/cli/components/SessionList.tsx
31867
33350
  init_sessions();
31868
- init_channels();
33351
+ init_spaces();
33352
+ init_db();
31869
33353
  import { jsxDEV } from "react/jsx-dev-runtime";
31870
- function SessionList({ agent, onSelect, onSelectChannel, onNew }) {
33354
+ function getSpaceUnreadCount(spaceName, agent) {
33355
+ const db2 = getDb();
33356
+ const row = db2.prepare("SELECT COUNT(*) as count FROM messages WHERE space = ? AND from_agent != ? AND read_at IS NULL").get(spaceName, agent);
33357
+ return row.count;
33358
+ }
33359
+ function SessionList({ agent, onSelect, onSelectSpace, onNew }) {
31871
33360
  const [sessions, setSessions] = useState3(() => listSessions(agent));
31872
- const [channels, setChannels] = useState3(() => listChannels());
33361
+ const [spaces, setSpaces] = useState3(() => listSpaces());
31873
33362
  useEffect3(() => {
31874
33363
  const timer = setInterval(() => {
31875
33364
  setSessions(listSessions(agent));
31876
- setChannels(listChannels());
33365
+ setSpaces(listSpaces());
31877
33366
  }, 1000);
31878
33367
  return () => clearInterval(timer);
31879
33368
  }, [agent]);
@@ -31881,11 +33370,36 @@ function SessionList({ agent, onSelect, onSelectChannel, onNew }) {
31881
33370
  if (input === "n")
31882
33371
  onNew();
31883
33372
  });
31884
- const channelItems = channels.map((ch) => ({
31885
- label: `#${ch.name}${ch.description ? ` \u2014 ${ch.description}` : ""} (${ch.message_count} msgs, ${ch.member_count} members)`,
31886
- value: `channel:${ch.name}`
31887
- }));
31888
- const dmSessions = sessions.filter((s) => !s.session_id.startsWith("channel:"));
33373
+ const topLevel = spaces.filter((sp) => !sp.parent_id);
33374
+ const children = spaces.filter((sp) => sp.parent_id);
33375
+ const spaceItems = [];
33376
+ for (const sp of topLevel) {
33377
+ const unread = getSpaceUnreadCount(sp.name, agent);
33378
+ const unreadBadge = unread > 0 ? ` (${unread} unread)` : "";
33379
+ spaceItems.push({
33380
+ label: `#${sp.name}${sp.description ? ` \u2014 ${sp.description}` : ""} ${sp.message_count} msgs${unreadBadge}`,
33381
+ value: `space:${sp.name}`
33382
+ });
33383
+ const directChildren = children.filter((c) => c.parent_id === sp.name);
33384
+ for (const child of directChildren) {
33385
+ const childUnread = getSpaceUnreadCount(child.name, agent);
33386
+ const childBadge = childUnread > 0 ? ` (${childUnread} unread)` : "";
33387
+ spaceItems.push({
33388
+ label: ` \u2514 #${child.name}${child.description ? ` \u2014 ${child.description}` : ""} ${child.message_count} msgs${childBadge}`,
33389
+ value: `space:${child.name}`
33390
+ });
33391
+ const grandChildren = children.filter((gc) => gc.parent_id === child.name);
33392
+ for (const gc of grandChildren) {
33393
+ const gcUnread = getSpaceUnreadCount(gc.name, agent);
33394
+ const gcBadge = gcUnread > 0 ? ` (${gcUnread} unread)` : "";
33395
+ spaceItems.push({
33396
+ label: ` \u2514 #${gc.name}${gc.description ? ` \u2014 ${gc.description}` : ""} ${gc.message_count} msgs${gcBadge}`,
33397
+ value: `space:${gc.name}`
33398
+ });
33399
+ }
33400
+ }
33401
+ }
33402
+ const dmSessions = sessions.filter((s) => !s.session_id.startsWith("space:"));
31889
33403
  const sessionItems = dmSessions.map((s) => {
31890
33404
  const others = s.participants.filter((p) => p !== agent).join(", ") || agent;
31891
33405
  const unread = s.unread_count > 0 ? ` (${s.unread_count} unread)` : "";
@@ -31894,7 +33408,7 @@ function SessionList({ agent, onSelect, onSelectChannel, onNew }) {
31894
33408
  value: s.session_id
31895
33409
  };
31896
33410
  });
31897
- const allItems = [...channelItems, ...sessionItems];
33411
+ const allItems = [...spaceItems, ...sessionItems];
31898
33412
  if (allItems.length === 0) {
31899
33413
  return /* @__PURE__ */ jsxDEV(Box3, {
31900
33414
  flexDirection: "column",
@@ -31972,8 +33486,8 @@ function SessionList({ agent, onSelect, onSelectChannel, onNew }) {
31972
33486
  /* @__PURE__ */ jsxDEV(SelectInput_default, {
31973
33487
  items: allItems,
31974
33488
  onSelect: (item) => {
31975
- if (item.value.startsWith("channel:")) {
31976
- onSelectChannel(item.value.slice(8));
33489
+ if (item.value.startsWith("space:")) {
33490
+ onSelectSpace(item.value.slice(6));
31977
33491
  } else {
31978
33492
  const session = dmSessions.find((s) => s.session_id === item.value);
31979
33493
  if (session)
@@ -31986,31 +33500,55 @@ function SessionList({ agent, onSelect, onSelectChannel, onNew }) {
31986
33500
  }
31987
33501
 
31988
33502
  // src/cli/components/ChatView.tsx
31989
- import { useState as useState5, useEffect as useEffect5 } from "react";
33503
+ import { useState as useState5, useEffect as useEffect5, useRef as useRef2 } from "react";
31990
33504
  import { Box as Box5, Text as Text6, useInput as useInput4 } from "ink";
31991
33505
  init_messages();
31992
33506
 
31993
33507
  // src/lib/poll.ts
31994
33508
  init_messages();
31995
- import { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
33509
+ import { useState as useState4, useEffect as useEffect4 } from "react";
31996
33510
  function startPolling(opts) {
31997
33511
  const interval = opts.interval_ms ?? 200;
31998
- let lastSeen = new Date().toISOString();
31999
33512
  let stopped = false;
32000
- const poll = () => {
32001
- if (stopped)
32002
- return;
32003
- const messages = readMessages({
33513
+ let inFlight = false;
33514
+ let lastSeenId = 0;
33515
+ const seedLastSeen = () => {
33516
+ const latest = readMessages({
32004
33517
  session_id: opts.session_id,
32005
33518
  to: opts.to_agent,
32006
- channel: opts.channel,
32007
- since: lastSeen
33519
+ space: opts.space,
33520
+ order: "desc",
33521
+ limit: 1
32008
33522
  });
32009
- if (messages.length > 0) {
32010
- lastSeen = messages[messages.length - 1].created_at;
32011
- opts.on_messages(messages);
33523
+ if (latest.length > 0) {
33524
+ lastSeenId = latest[0].id;
33525
+ }
33526
+ };
33527
+ const poll = () => {
33528
+ if (stopped || inFlight)
33529
+ return;
33530
+ inFlight = true;
33531
+ try {
33532
+ const messages = readMessages({
33533
+ session_id: opts.session_id,
33534
+ to: opts.to_agent,
33535
+ space: opts.space,
33536
+ since_id: lastSeenId,
33537
+ order: "asc"
33538
+ });
33539
+ if (messages.length > 0) {
33540
+ lastSeenId = messages[messages.length - 1].id;
33541
+ try {
33542
+ opts.on_messages(messages);
33543
+ } catch (error) {
33544
+ console.error("Polling callback error:", error);
33545
+ }
33546
+ }
33547
+ } finally {
33548
+ inFlight = false;
32012
33549
  }
32013
33550
  };
33551
+ seedLastSeen();
32014
33552
  const timer = setInterval(poll, interval);
32015
33553
  return {
32016
33554
  stop: () => {
@@ -32059,38 +33597,51 @@ function MessageBubble({ message, isOwn }) {
32059
33597
 
32060
33598
  // src/cli/components/ChatView.tsx
32061
33599
  import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
32062
- function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, channelName }) {
33600
+ function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, spaceName }) {
32063
33601
  const [messages, setMessages] = useState5([]);
32064
33602
  const [input, setInput] = useState5("");
32065
33603
  const [sessionId, setSessionId] = useState5(initialSessionId);
32066
- const isChannel = !!channelName;
33604
+ const isSpace = !!spaceName;
33605
+ const seenIds = useRef2(new Set);
32067
33606
  useEffect5(() => {
32068
- const opts = isChannel ? { channel: channelName } : sessionId ? { session_id: sessionId } : {};
32069
- if (isChannel || sessionId) {
33607
+ seenIds.current = new Set;
33608
+ const opts = isSpace ? { space: spaceName } : sessionId ? { session_id: sessionId } : {};
33609
+ if (isSpace || sessionId) {
32070
33610
  const existing = readMessages(opts);
33611
+ for (const msg of existing) {
33612
+ seenIds.current.add(msg.id);
33613
+ }
32071
33614
  setMessages(existing);
33615
+ } else {
33616
+ setMessages([]);
32072
33617
  }
32073
- const pollOpts = isChannel ? { channel: channelName } : sessionId ? { session_id: sessionId } : null;
33618
+ const pollOpts = isSpace ? { space: spaceName } : sessionId ? { session_id: sessionId } : null;
32074
33619
  if (!pollOpts)
32075
33620
  return;
32076
33621
  const { stop } = startPolling({
32077
33622
  ...pollOpts,
32078
33623
  interval_ms: 200,
32079
33624
  on_messages: (newMsgs) => {
32080
- setMessages((prev) => [...prev, ...newMsgs]);
33625
+ const unseen = newMsgs.filter((msg) => !seenIds.current.has(msg.id));
33626
+ if (unseen.length === 0)
33627
+ return;
33628
+ for (const msg of unseen) {
33629
+ seenIds.current.add(msg.id);
33630
+ }
33631
+ setMessages((prev) => [...prev, ...unseen]);
32081
33632
  }
32082
33633
  });
32083
33634
  return stop;
32084
- }, [sessionId, channelName]);
33635
+ }, [sessionId, spaceName]);
32085
33636
  useEffect5(() => {
32086
33637
  if (messages.length === 0)
32087
33638
  return;
32088
- if (isChannel && channelName) {
32089
- markChannelRead(channelName, agent);
33639
+ if (isSpace && spaceName) {
33640
+ markSpaceRead(spaceName, agent);
32090
33641
  } else if (sessionId) {
32091
33642
  markSessionRead(sessionId, agent);
32092
33643
  }
32093
- }, [messages.length]);
33644
+ }, [messages.length, isSpace, spaceName, sessionId, agent]);
32094
33645
  useInput4((_, key) => {
32095
33646
  if (key.escape)
32096
33647
  onBack();
@@ -32098,14 +33649,15 @@ function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, chann
32098
33649
  const handleSubmit = (value) => {
32099
33650
  if (!value.trim())
32100
33651
  return;
32101
- if (isChannel && channelName) {
33652
+ if (isSpace && spaceName) {
32102
33653
  const msg = sendMessage({
32103
33654
  from: agent,
32104
- to: channelName,
33655
+ to: spaceName,
32105
33656
  content: value.trim(),
32106
- channel: channelName,
32107
- session_id: `channel:${channelName}`
33657
+ space: spaceName,
33658
+ session_id: `space:${spaceName}`
32108
33659
  });
33660
+ seenIds.current.add(msg.id);
32109
33661
  setMessages((prev) => [...prev, msg]);
32110
33662
  } else {
32111
33663
  const to = recipient || agent;
@@ -32115,14 +33667,16 @@ function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, chann
32115
33667
  content: value.trim(),
32116
33668
  session_id: sessionId
32117
33669
  });
33670
+ seenIds.current.add(msg.id);
33671
+ setMessages((prev) => [...prev, msg]);
32118
33672
  if (!sessionId) {
32119
33673
  setSessionId(msg.session_id);
32120
33674
  }
32121
33675
  }
32122
33676
  setInput("");
32123
33677
  };
32124
- const title = isChannel ? `#${channelName}` : recipient || "self";
32125
- const prompt = isChannel ? `${agent} \u2192 #${channelName}` : `${agent} \u2192 ${recipient || "self"}`;
33678
+ const title = isSpace ? `#${spaceName}` : recipient || "self";
33679
+ const prompt = isSpace ? `${agent} \u2192 #${spaceName}` : `${agent} \u2192 ${recipient || "self"}`;
32126
33680
  return /* @__PURE__ */ jsxDEV3(Box5, {
32127
33681
  flexDirection: "column",
32128
33682
  padding: 1,
@@ -32132,7 +33686,7 @@ function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, chann
32132
33686
  children: [
32133
33687
  /* @__PURE__ */ jsxDEV3(Text6, {
32134
33688
  bold: true,
32135
- color: isChannel ? "magenta" : "cyan",
33689
+ color: isSpace ? "magenta" : "cyan",
32136
33690
  children: title
32137
33691
  }, undefined, false, undefined, this),
32138
33692
  /* @__PURE__ */ jsxDEV3(Text6, {
@@ -32156,7 +33710,7 @@ function ChatView({ agent, onBack, sessionId: initialSessionId, recipient, chann
32156
33710
  marginTop: 1,
32157
33711
  children: [
32158
33712
  /* @__PURE__ */ jsxDEV3(Text6, {
32159
- color: isChannel ? "magenta" : "cyan",
33713
+ color: isSpace ? "magenta" : "cyan",
32160
33714
  children: [
32161
33715
  prompt,
32162
33716
  ": "
@@ -32180,7 +33734,7 @@ function App({ agent }) {
32180
33734
  const { exit } = useApp();
32181
33735
  const [view, setView] = useState6("sessions");
32182
33736
  const [currentSession, setCurrentSession] = useState6(null);
32183
- const [currentChannel, setCurrentChannel] = useState6(null);
33737
+ const [currentSpace, setCurrentSpace] = useState6(null);
32184
33738
  const [newTo, setNewTo] = useState6("");
32185
33739
  useInput5((input, key) => {
32186
33740
  if (input === "q" && view === "sessions") {
@@ -32195,9 +33749,9 @@ function App({ agent }) {
32195
33749
  setCurrentSession(session);
32196
33750
  setView("chat");
32197
33751
  };
32198
- const handleSelectChannel = (channelName) => {
32199
- setCurrentChannel(channelName);
32200
- setView("channel");
33752
+ const handleSelectSpace = (spaceName) => {
33753
+ setCurrentSpace(spaceName);
33754
+ setView("space");
32201
33755
  };
32202
33756
  const handleNewConversation = () => {
32203
33757
  setView("new");
@@ -32217,7 +33771,7 @@ function App({ agent }) {
32217
33771
  };
32218
33772
  const handleBack = () => {
32219
33773
  setCurrentSession(null);
32220
- setCurrentChannel(null);
33774
+ setCurrentSpace(null);
32221
33775
  setView("sessions");
32222
33776
  };
32223
33777
  if (view === "new") {
@@ -32251,10 +33805,10 @@ function App({ agent }) {
32251
33805
  ]
32252
33806
  }, undefined, true, undefined, this);
32253
33807
  }
32254
- if (view === "channel" && currentChannel) {
33808
+ if (view === "space" && currentSpace) {
32255
33809
  return /* @__PURE__ */ jsxDEV4(ChatView, {
32256
33810
  agent,
32257
- channelName: currentChannel,
33811
+ spaceName: currentSpace,
32258
33812
  onBack: handleBack
32259
33813
  }, undefined, false, undefined, this);
32260
33814
  }
@@ -32270,22 +33824,45 @@ function App({ agent }) {
32270
33824
  return /* @__PURE__ */ jsxDEV4(SessionList, {
32271
33825
  agent,
32272
33826
  onSelect: handleSelectSession,
32273
- onSelectChannel: handleSelectChannel,
33827
+ onSelectSpace: handleSelectSpace,
32274
33828
  onNew: handleNewConversation
32275
33829
  }, undefined, false, undefined, this);
32276
33830
  }
32277
33831
 
32278
33832
  // src/cli/index.tsx
32279
33833
  var program2 = new Command;
32280
- program2.name("conversations").description("Real-time CLI messaging for AI agents").version("0.0.7");
33834
+ program2.name("conversations").description("Real-time CLI messaging for AI agents").version("0.1.0");
32281
33835
  program2.command("send").description("Send a message to an agent").argument("<message>", "Message content").requiredOption("--to <agent>", "Recipient agent ID").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("--json", "Output as JSON").action((message, opts) => {
32282
- const from = resolveIdentity(opts.from);
32283
- const metadata = opts.metadata ? JSON.parse(opts.metadata) : undefined;
33836
+ const from = resolveIdentity(opts.from).trim();
33837
+ const to = typeof opts.to === "string" ? opts.to.trim() : "";
33838
+ const content = typeof message === "string" ? message : "";
33839
+ const session = typeof opts.session === "string" && opts.session.trim() ? opts.session.trim() : undefined;
33840
+ if (!from) {
33841
+ console.error(chalk2.red("Sender identity is required."));
33842
+ process.exit(1);
33843
+ }
33844
+ if (!to) {
33845
+ console.error(chalk2.red("Recipient is required."));
33846
+ process.exit(1);
33847
+ }
33848
+ if (!content.trim()) {
33849
+ console.error(chalk2.red("Message content cannot be empty."));
33850
+ process.exit(1);
33851
+ }
33852
+ let metadata;
33853
+ if (opts.metadata) {
33854
+ try {
33855
+ metadata = JSON.parse(opts.metadata);
33856
+ } catch {
33857
+ console.error(chalk2.red("Invalid --metadata JSON."));
33858
+ process.exit(1);
33859
+ }
33860
+ }
32284
33861
  const msg = sendMessage({
32285
33862
  from,
32286
- to: opts.to,
32287
- content: message,
32288
- session_id: opts.session,
33863
+ to,
33864
+ content,
33865
+ session_id: session,
32289
33866
  priority: opts.priority,
32290
33867
  working_dir: opts.workingDir,
32291
33868
  repository: opts.repository,
@@ -32299,31 +33876,71 @@ program2.command("send").description("Send a message to an agent").argument("<me
32299
33876
  }
32300
33877
  closeDb();
32301
33878
  });
32302
- program2.command("read").description("Read messages").option("--session <id>", "Filter by session ID").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--channel <name>", "Filter by channel").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--unread", "Only unread messages").option("--mark-read", "Mark returned messages as read").option("--json", "Output as JSON").action((opts) => {
33879
+ program2.command("read").description("Read messages").option("--session <id>", "Filter by session ID").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--space <name>", "Filter by space").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--unread", "Only unread messages").option("--mark-read", "Mark returned messages as read").option("--json", "Output as JSON").action((opts) => {
32303
33880
  const messages = readMessages({
32304
33881
  session_id: opts.session,
32305
33882
  from: opts.from,
32306
33883
  to: opts.to,
32307
- channel: opts.channel,
33884
+ space: opts.space,
32308
33885
  since: opts.since,
32309
33886
  limit: opts.limit,
32310
33887
  unread_only: opts.unread
32311
33888
  });
32312
- if (opts.markRead && opts.to) {
32313
- const ids = messages.filter((m) => m.to_agent === opts.to && !m.read_at).map((m) => m.id);
32314
- if (ids.length > 0)
32315
- markRead(ids, opts.to);
33889
+ if (opts.markRead) {
33890
+ const reader = resolveIdentity(opts.to);
33891
+ if (opts.space) {
33892
+ markSpaceRead(opts.space, reader);
33893
+ } else if (opts.session) {
33894
+ markSessionRead(opts.session, reader);
33895
+ } else {
33896
+ const ids = messages.filter((m) => m.to_agent === reader && !m.read_at).map((m) => m.id);
33897
+ if (ids.length > 0)
33898
+ markRead(ids, reader);
33899
+ }
33900
+ }
33901
+ if (opts.json) {
33902
+ console.log(JSON.stringify(messages, null, 2));
33903
+ } else {
33904
+ if (messages.length === 0) {
33905
+ console.log(chalk2.dim("No messages found."));
33906
+ } else {
33907
+ for (const msg of messages) {
33908
+ const time3 = chalk2.dim(msg.created_at.slice(11, 19));
33909
+ const from = chalk2.cyan(msg.from_agent);
33910
+ const to = msg.space ? chalk2.magenta(`#${msg.space}`) : chalk2.yellow(msg.to_agent);
33911
+ const priority = msg.priority !== "normal" ? chalk2.red(` [${msg.priority}]`) : "";
33912
+ const unread = !msg.read_at ? chalk2.green(" *") : "";
33913
+ console.log(`${time3} ${from} \u2192 ${to}${priority}${unread}: ${msg.content}`);
33914
+ }
33915
+ }
32316
33916
  }
33917
+ closeDb();
33918
+ });
33919
+ program2.command("search").description("Search messages by content").argument("<query>", "Search query string").option("--space <name>", "Filter by space").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--limit <n>", "Max results to return", parseInt).option("--json", "Output as JSON").action((query, opts) => {
33920
+ const q = typeof query === "string" ? query.trim() : "";
33921
+ if (!q) {
33922
+ console.error(chalk2.red("Search query cannot be empty."));
33923
+ process.exit(1);
33924
+ }
33925
+ const messages = searchMessages({
33926
+ query: q,
33927
+ space: opts.space,
33928
+ from: opts.from,
33929
+ to: opts.to,
33930
+ limit: opts.limit
33931
+ });
32317
33932
  if (opts.json) {
32318
33933
  console.log(JSON.stringify(messages, null, 2));
32319
33934
  } else {
32320
33935
  if (messages.length === 0) {
32321
33936
  console.log(chalk2.dim("No messages found."));
32322
33937
  } else {
33938
+ console.log(chalk2.dim(`Found ${messages.length} result(s) for "${q}":
33939
+ `));
32323
33940
  for (const msg of messages) {
32324
33941
  const time3 = chalk2.dim(msg.created_at.slice(11, 19));
32325
33942
  const from = chalk2.cyan(msg.from_agent);
32326
- const to = msg.channel ? chalk2.magenta(`#${msg.channel}`) : chalk2.yellow(msg.to_agent);
33943
+ const to = msg.space ? chalk2.magenta(`#${msg.space}`) : chalk2.yellow(msg.to_agent);
32327
33944
  const priority = msg.priority !== "normal" ? chalk2.red(` [${msg.priority}]`) : "";
32328
33945
  const unread = !msg.read_at ? chalk2.green(" *") : "";
32329
33946
  console.log(`${time3} ${from} \u2192 ${to}${priority}${unread}: ${msg.content}`);
@@ -32355,13 +33972,25 @@ program2.command("reply").description("Reply to a message (uses same session)").
32355
33972
  console.error(chalk2.red(`Message #${opts.to} not found.`));
32356
33973
  process.exit(1);
32357
33974
  }
32358
- const from = resolveIdentity(opts.from);
33975
+ const from = resolveIdentity(opts.from).trim();
33976
+ const content = typeof message === "string" ? message : "";
33977
+ if (!from) {
33978
+ console.error(chalk2.red("Sender identity is required."));
33979
+ process.exit(1);
33980
+ }
33981
+ if (!content.trim()) {
33982
+ console.error(chalk2.red("Reply content cannot be empty."));
33983
+ process.exit(1);
33984
+ }
33985
+ const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
33986
+ const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
32359
33987
  const msg = sendMessage({
32360
33988
  from,
32361
- to: original.from_agent,
32362
- content: message,
33989
+ to,
33990
+ content,
32363
33991
  session_id: original.session_id,
32364
- priority: opts.priority
33992
+ priority: opts.priority,
33993
+ space
32365
33994
  });
32366
33995
  if (opts.json) {
32367
33996
  console.log(JSON.stringify(msg, null, 2));
@@ -32370,17 +33999,19 @@ program2.command("reply").description("Reply to a message (uses same session)").
32370
33999
  }
32371
34000
  closeDb();
32372
34001
  });
32373
- program2.command("mark-read").description("Mark messages as read").argument("[ids...]", "Message IDs to mark as read").option("--session <id>", "Mark all messages in session as read").option("--channel <name>", "Mark all messages in channel as read").option("--agent <id>", "Agent marking messages as read").option("--json", "Output as JSON").action((ids, opts) => {
34002
+ program2.command("mark-read").description("Mark messages as read").argument("[ids...]", "Message IDs to mark as read").option("--all", "Mark all messages as read").option("--session <id>", "Mark all messages in session as read").option("--space <name>", "Mark all messages in space as read").option("--agent <id>", "Agent marking messages as read").option("--json", "Output as JSON").action((ids, opts) => {
32374
34003
  const agent = resolveIdentity(opts.agent);
32375
34004
  let count = 0;
32376
- if (opts.session) {
34005
+ if (opts.all) {
34006
+ count = markAllRead(agent);
34007
+ } else if (opts.session) {
32377
34008
  count = markSessionRead(opts.session, agent);
32378
- } else if (opts.channel) {
32379
- count = markChannelRead(opts.channel, agent);
34009
+ } else if (opts.space) {
34010
+ count = markSpaceRead(opts.space, agent);
32380
34011
  } else if (ids.length > 0) {
32381
34012
  count = markRead(ids.map(Number), agent);
32382
34013
  } else {
32383
- console.error(chalk2.red("Provide message IDs, --session, or --channel flag."));
34014
+ console.error(chalk2.red("Provide message IDs, --all, --session, or --space flag."));
32384
34015
  process.exit(1);
32385
34016
  }
32386
34017
  if (opts.json) {
@@ -32390,18 +34021,33 @@ program2.command("mark-read").description("Mark messages as read").argument("[id
32390
34021
  }
32391
34022
  closeDb();
32392
34023
  });
34024
+ program2.command("export").description("Export messages as JSON or CSV").option("--space <name>", "Filter by space").option("--session <id>", "Filter by session ID").option("--from <agent>", "Filter by sender").option("--since <date>", "Messages after this ISO date").option("--until <date>", "Messages before this ISO date").option("--format <format>", "Output format: json or csv", "json").action((opts) => {
34025
+ const format = opts.format === "csv" ? "csv" : "json";
34026
+ const result = exportMessages({
34027
+ space: opts.space,
34028
+ session_id: opts.session,
34029
+ from: opts.from,
34030
+ since: opts.since,
34031
+ until: opts.until,
34032
+ format
34033
+ });
34034
+ console.log(result);
34035
+ closeDb();
34036
+ });
32393
34037
  program2.command("status").description("Show database stats").option("--json", "Output as JSON").action((opts) => {
32394
34038
  const db2 = getDb();
32395
34039
  const dbPath = getDbPath();
32396
34040
  const totalMessages = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
32397
34041
  const totalSessions = db2.prepare("SELECT COUNT(DISTINCT session_id) as count FROM messages").get().count;
32398
34042
  const totalUnread = db2.prepare("SELECT COUNT(*) as count FROM messages WHERE read_at IS NULL").get().count;
32399
- const totalChannels = db2.prepare("SELECT COUNT(*) as count FROM channels").get().count;
34043
+ const totalSpaces = db2.prepare("SELECT COUNT(*) as count FROM spaces").get().count;
34044
+ const totalProjects = db2.prepare("SELECT COUNT(*) as count FROM projects").get().count;
32400
34045
  const stats = {
32401
34046
  db_path: dbPath,
32402
34047
  total_messages: totalMessages,
32403
34048
  total_sessions: totalSessions,
32404
- total_channels: totalChannels,
34049
+ total_spaces: totalSpaces,
34050
+ total_projects: totalProjects,
32405
34051
  unread_messages: totalUnread
32406
34052
  };
32407
34053
  if (opts.json) {
@@ -32411,71 +34057,236 @@ program2.command("status").description("Show database stats").option("--json", "
32411
34057
  console.log(` DB Path: ${stats.db_path}`);
32412
34058
  console.log(` Messages: ${stats.total_messages}`);
32413
34059
  console.log(` Sessions: ${stats.total_sessions}`);
32414
- console.log(` Channels: ${stats.total_channels}`);
34060
+ console.log(` Spaces: ${stats.total_spaces}`);
34061
+ console.log(` Projects: ${stats.total_projects}`);
32415
34062
  console.log(` Unread: ${stats.unread_messages}`);
32416
34063
  }
32417
34064
  closeDb();
32418
34065
  });
32419
- var channel = program2.command("channel").description("Manage channels");
32420
- channel.command("create").description("Create a new channel").argument("<name>", "Channel name").option("--description <text>", "Channel description").option("--from <agent>", "Creator agent ID").option("--json", "Output as JSON").action((name, opts) => {
32421
- const agent = resolveIdentity(opts.from);
34066
+ program2.command("update").description("Check for and install updates").option("--check", "Only check for updates, don't install").option("--json", "Output as JSON").action(async (opts) => {
34067
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
34068
+ const current = pkg.version;
34069
+ let latest;
34070
+ try {
34071
+ const res = await fetch("https://registry.npmjs.org/@hasna/conversations/latest");
34072
+ const data = await res.json();
34073
+ latest = data.version;
34074
+ } catch {
34075
+ if (opts.json) {
34076
+ console.log(JSON.stringify({ error: "Failed to check npm registry" }));
34077
+ } else {
34078
+ console.error(chalk2.red("Failed to check npm registry for updates."));
34079
+ }
34080
+ process.exit(1);
34081
+ }
34082
+ const updateAvailable = current !== latest;
34083
+ if (opts.check || !updateAvailable) {
34084
+ if (opts.json) {
34085
+ console.log(JSON.stringify({ current, latest, updateAvailable }));
34086
+ } else if (updateAvailable) {
34087
+ console.log(`Current version: ${chalk2.yellow(current)}`);
34088
+ console.log(`Latest version: ${chalk2.green(latest)}`);
34089
+ console.log(chalk2.cyan(`Run ${chalk2.bold("conversations update")} to install.`));
34090
+ } else {
34091
+ console.log(chalk2.green(`Already on latest version (${current})`));
34092
+ }
34093
+ return;
34094
+ }
34095
+ if (opts.json) {
34096
+ console.log(JSON.stringify({ current, latest, updateAvailable, status: "updating" }));
34097
+ } else {
34098
+ console.log(`Updating from ${chalk2.yellow(current)} to ${chalk2.green(latest)}...`);
34099
+ }
34100
+ const proc = Bun.spawn(["bun", "install", "-g", `@hasna/conversations@${latest}`], {
34101
+ stdout: "inherit",
34102
+ stderr: "inherit"
34103
+ });
34104
+ const exitCode = await proc.exited;
34105
+ if (exitCode === 0) {
34106
+ if (!opts.json) {
34107
+ console.log(chalk2.green(`
34108
+ Successfully updated to v${latest}`));
34109
+ }
34110
+ } else {
34111
+ if (opts.json) {
34112
+ console.log(JSON.stringify({ error: "Update failed", exitCode }));
34113
+ } else {
34114
+ console.error(chalk2.red(`
34115
+ Update failed (exit code ${exitCode})`));
34116
+ }
34117
+ process.exit(1);
34118
+ }
34119
+ });
34120
+ var space = program2.command("space").description("Manage spaces");
34121
+ space.command("create").description("Create a new space").argument("<name>", "Space name").option("--description <text>", "Space description").option("--parent <name>", "Parent space name (for nesting)").option("--project <id>", "Project ID to associate with").option("--from <agent>", "Creator agent ID").option("--json", "Output as JSON").action((name, opts) => {
34122
+ const agent = resolveIdentity(opts.from).trim();
34123
+ const spaceName = typeof name === "string" ? name.trim() : "";
34124
+ if (!agent) {
34125
+ console.error(chalk2.red("Creator identity is required."));
34126
+ process.exit(1);
34127
+ }
34128
+ if (!spaceName) {
34129
+ console.error(chalk2.red("Space name cannot be empty."));
34130
+ process.exit(1);
34131
+ }
32422
34132
  try {
32423
- const ch = createChannel(name, agent, opts.description);
34133
+ const description = typeof opts.description === "string" && opts.description.trim() ? opts.description.trim() : undefined;
34134
+ const sp = createSpace(spaceName, agent, {
34135
+ description,
34136
+ parent_id: opts.parent,
34137
+ project_id: opts.project
34138
+ });
32424
34139
  if (opts.json) {
32425
- console.log(JSON.stringify(ch, null, 2));
34140
+ console.log(JSON.stringify(sp, null, 2));
32426
34141
  } else {
32427
- console.log(chalk2.green(`Channel #${ch.name} created`) + (ch.description ? chalk2.dim(` \u2014 ${ch.description}`) : ""));
34142
+ console.log(chalk2.green(`Space #${sp.name} created`) + (sp.description ? chalk2.dim(` \u2014 ${sp.description}`) : ""));
32428
34143
  }
32429
34144
  } catch (e) {
32430
34145
  if (e.message?.includes("UNIQUE constraint")) {
32431
- console.error(chalk2.red(`Channel #${name} already exists.`));
34146
+ console.error(chalk2.red(`Space #${spaceName} already exists.`));
32432
34147
  process.exit(1);
32433
34148
  }
32434
- throw e;
34149
+ console.error(chalk2.red(e.message));
34150
+ process.exit(1);
32435
34151
  }
32436
34152
  closeDb();
32437
34153
  });
32438
- channel.command("list").description("List all channels").option("--json", "Output as JSON").action((opts) => {
32439
- const channels = listChannels();
34154
+ space.command("list").description("List all spaces").option("--project <id>", "Filter by project ID").option("--parent <name>", "Filter by parent space name").option("--top-level", "Show only top-level spaces").option("--archived", "Include archived spaces").option("--json", "Output as JSON").action((opts) => {
34155
+ const listOpts = {};
34156
+ if (opts.project)
34157
+ listOpts.project_id = opts.project;
34158
+ if (opts.topLevel) {
34159
+ listOpts.parent_id = null;
34160
+ } else if (opts.parent) {
34161
+ listOpts.parent_id = opts.parent;
34162
+ }
34163
+ if (opts.archived)
34164
+ listOpts.include_archived = true;
34165
+ const spaces = listSpaces(listOpts);
32440
34166
  if (opts.json) {
32441
- console.log(JSON.stringify(channels, null, 2));
34167
+ console.log(JSON.stringify(spaces, null, 2));
32442
34168
  } else {
32443
- if (channels.length === 0) {
32444
- console.log(chalk2.dim("No channels found."));
34169
+ if (spaces.length === 0) {
34170
+ console.log(chalk2.dim("No spaces found."));
32445
34171
  } else {
32446
- for (const ch of channels) {
32447
- const desc = ch.description ? chalk2.dim(` \u2014 ${ch.description}`) : "";
32448
- console.log(`${chalk2.magenta(`#${ch.name}`)}${desc} ${ch.member_count} members, ${ch.message_count} messages`);
34172
+ for (const sp of spaces) {
34173
+ const desc = sp.description ? chalk2.dim(` \u2014 ${sp.description}`) : "";
34174
+ const parent = sp.parent_id ? chalk2.dim(` (child of ${sp.parent_id})`) : "";
34175
+ const archived = sp.archived_at ? chalk2.yellow(" [archived]") : "";
34176
+ console.log(`${chalk2.magenta(`#${sp.name}`)}${desc}${parent}${archived} ${sp.member_count} members, ${sp.message_count} messages`);
32449
34177
  }
32450
34178
  }
32451
34179
  }
32452
34180
  closeDb();
32453
34181
  });
32454
- channel.command("send").description("Send a message to a channel").argument("<channel>", "Channel name").argument("<message>", "Message content").option("--from <agent>", "Sender agent ID").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--json", "Output as JSON").action((channelName, message, opts) => {
32455
- const from = resolveIdentity(opts.from);
32456
- const ch = getChannel(channelName);
32457
- if (!ch) {
32458
- console.error(chalk2.red(`Channel #${channelName} not found.`));
34182
+ space.command("update").description("Update a space").argument("<name>", "Space name").option("--description <text>", "New description").option("--parent <name>", "New parent space name").option("--project <id>", "New project ID").option("--json", "Output as JSON").action((name, opts) => {
34183
+ const spaceName = typeof name === "string" ? name.trim() : "";
34184
+ if (!spaceName) {
34185
+ console.error(chalk2.red("Space name cannot be empty."));
34186
+ process.exit(1);
34187
+ }
34188
+ const updates = {};
34189
+ if (opts.description !== undefined)
34190
+ updates.description = opts.description;
34191
+ if (opts.parent !== undefined)
34192
+ updates.parent_id = opts.parent || null;
34193
+ if (opts.project !== undefined)
34194
+ updates.project_id = opts.project || null;
34195
+ try {
34196
+ const sp = updateSpace(spaceName, updates);
34197
+ if (opts.json) {
34198
+ console.log(JSON.stringify(sp, null, 2));
34199
+ } else {
34200
+ console.log(chalk2.green(`Space #${sp.name} updated.`));
34201
+ }
34202
+ } catch (e) {
34203
+ console.error(chalk2.red(e.message));
34204
+ process.exit(1);
34205
+ }
34206
+ closeDb();
34207
+ });
34208
+ space.command("archive").description("Archive a space").argument("<name>", "Space name").option("--json", "Output as JSON").action((name, opts) => {
34209
+ const spaceName = typeof name === "string" ? name.trim() : "";
34210
+ if (!spaceName) {
34211
+ console.error(chalk2.red("Space name cannot be empty."));
34212
+ process.exit(1);
34213
+ }
34214
+ try {
34215
+ const sp = archiveSpace(spaceName);
34216
+ if (opts.json) {
34217
+ console.log(JSON.stringify(sp, null, 2));
34218
+ } else {
34219
+ console.log(chalk2.green(`Space #${sp.name} archived.`));
34220
+ }
34221
+ } catch (e) {
34222
+ console.error(chalk2.red(e.message));
34223
+ process.exit(1);
34224
+ }
34225
+ closeDb();
34226
+ });
34227
+ space.command("unarchive").description("Unarchive a space").argument("<name>", "Space name").option("--json", "Output as JSON").action((name, opts) => {
34228
+ const spaceName = typeof name === "string" ? name.trim() : "";
34229
+ if (!spaceName) {
34230
+ console.error(chalk2.red("Space name cannot be empty."));
34231
+ process.exit(1);
34232
+ }
34233
+ try {
34234
+ const sp = unarchiveSpace(spaceName);
34235
+ if (opts.json) {
34236
+ console.log(JSON.stringify(sp, null, 2));
34237
+ } else {
34238
+ console.log(chalk2.green(`Space #${sp.name} unarchived.`));
34239
+ }
34240
+ } catch (e) {
34241
+ console.error(chalk2.red(e.message));
34242
+ process.exit(1);
34243
+ }
34244
+ closeDb();
34245
+ });
34246
+ space.command("send").description("Send a message to a space").argument("<space>", "Space name").argument("<message>", "Message content").option("--from <agent>", "Sender agent ID").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--json", "Output as JSON").action((spaceName, message, opts) => {
34247
+ const from = resolveIdentity(opts.from).trim();
34248
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
34249
+ const content = typeof message === "string" ? message : "";
34250
+ if (!from) {
34251
+ console.error(chalk2.red("Sender identity is required."));
34252
+ process.exit(1);
34253
+ }
34254
+ if (!spaceArg) {
34255
+ console.error(chalk2.red("Space name cannot be empty."));
34256
+ process.exit(1);
34257
+ }
34258
+ if (!content.trim()) {
34259
+ console.error(chalk2.red("Message content cannot be empty."));
34260
+ process.exit(1);
34261
+ }
34262
+ const sp = getSpace(spaceArg);
34263
+ if (!sp) {
34264
+ console.error(chalk2.red(`Space #${spaceArg} not found.`));
32459
34265
  process.exit(1);
32460
34266
  }
32461
34267
  const msg = sendMessage({
32462
34268
  from,
32463
- to: channelName,
32464
- content: message,
32465
- channel: channelName,
32466
- session_id: `channel:${channelName}`,
34269
+ to: spaceArg,
34270
+ content,
34271
+ space: spaceArg,
34272
+ session_id: `space:${spaceArg}`,
32467
34273
  priority: opts.priority
32468
34274
  });
32469
34275
  if (opts.json) {
32470
34276
  console.log(JSON.stringify(msg, null, 2));
32471
34277
  } else {
32472
- console.log(chalk2.green(`Message sent to #${channelName}`) + chalk2.dim(` (id: ${msg.id})`));
34278
+ console.log(chalk2.green(`Message sent to #${spaceArg}`) + chalk2.dim(` (id: ${msg.id})`));
32473
34279
  }
32474
34280
  closeDb();
32475
34281
  });
32476
- channel.command("read").description("Read messages from a channel").argument("<channel>", "Channel name").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--json", "Output as JSON").action((channelName, opts) => {
34282
+ space.command("read").description("Read messages from a space").argument("<space>", "Space name").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("--json", "Output as JSON").action((spaceName, opts) => {
34283
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
34284
+ if (!spaceArg) {
34285
+ console.error(chalk2.red("Space name cannot be empty."));
34286
+ process.exit(1);
34287
+ }
32477
34288
  const messages = readMessages({
32478
- channel: channelName,
34289
+ space: spaceArg,
32479
34290
  since: opts.since,
32480
34291
  limit: opts.limit
32481
34292
  });
@@ -32483,55 +34294,78 @@ channel.command("read").description("Read messages from a channel").argument("<c
32483
34294
  console.log(JSON.stringify(messages, null, 2));
32484
34295
  } else {
32485
34296
  if (messages.length === 0) {
32486
- console.log(chalk2.dim(`No messages in #${channelName}.`));
34297
+ console.log(chalk2.dim(`No messages in #${spaceArg}.`));
32487
34298
  } else {
32488
34299
  for (const msg of messages) {
32489
34300
  const time3 = chalk2.dim(msg.created_at.slice(11, 19));
32490
34301
  const from = chalk2.cyan(msg.from_agent);
32491
34302
  const priority = msg.priority !== "normal" ? chalk2.red(` [${msg.priority}]`) : "";
32492
- console.log(`${time3} ${from} \u2192 ${chalk2.magenta(`#${channelName}`)}${priority}: ${msg.content}`);
34303
+ console.log(`${time3} ${from} \u2192 ${chalk2.magenta(`#${spaceArg}`)}${priority}: ${msg.content}`);
32493
34304
  }
32494
34305
  }
32495
34306
  }
32496
34307
  closeDb();
32497
34308
  });
32498
- channel.command("join").description("Join a channel").argument("<channel>", "Channel name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((channelName, opts) => {
32499
- const agent = resolveIdentity(opts.from);
32500
- const ok = joinChannel(channelName, agent);
34309
+ space.command("join").description("Join a space").argument("<space>", "Space name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((spaceName, opts) => {
34310
+ const agent = resolveIdentity(opts.from).trim();
34311
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
34312
+ if (!agent) {
34313
+ console.error(chalk2.red("Agent identity is required."));
34314
+ process.exit(1);
34315
+ }
34316
+ if (!spaceArg) {
34317
+ console.error(chalk2.red("Space name cannot be empty."));
34318
+ process.exit(1);
34319
+ }
34320
+ const ok = joinSpace(spaceArg, agent);
32501
34321
  if (!ok) {
32502
- console.error(chalk2.red(`Channel #${channelName} not found.`));
34322
+ console.error(chalk2.red(`Space #${spaceArg} not found.`));
32503
34323
  process.exit(1);
32504
34324
  }
32505
34325
  if (opts.json) {
32506
- console.log(JSON.stringify({ channel: channelName, agent, joined: true }));
34326
+ console.log(JSON.stringify({ space: spaceArg, agent, joined: true }));
32507
34327
  } else {
32508
- console.log(chalk2.green(`${agent} joined #${channelName}`));
34328
+ console.log(chalk2.green(`${agent} joined #${spaceArg}`));
32509
34329
  }
32510
34330
  closeDb();
32511
34331
  });
32512
- channel.command("leave").description("Leave a channel").argument("<channel>", "Channel name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((channelName, opts) => {
32513
- const agent = resolveIdentity(opts.from);
32514
- const ok = leaveChannel(channelName, agent);
34332
+ space.command("leave").description("Leave a space").argument("<space>", "Space name").option("--from <agent>", "Agent ID").option("--json", "Output as JSON").action((spaceName, opts) => {
34333
+ const agent = resolveIdentity(opts.from).trim();
34334
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
34335
+ if (!agent) {
34336
+ console.error(chalk2.red("Agent identity is required."));
34337
+ process.exit(1);
34338
+ }
34339
+ if (!spaceArg) {
34340
+ console.error(chalk2.red("Space name cannot be empty."));
34341
+ process.exit(1);
34342
+ }
34343
+ const ok = leaveSpace(spaceArg, agent);
32515
34344
  if (opts.json) {
32516
- console.log(JSON.stringify({ channel: channelName, agent, left: ok }));
34345
+ console.log(JSON.stringify({ space: spaceArg, agent, left: ok }));
32517
34346
  } else {
32518
34347
  if (ok) {
32519
- console.log(chalk2.green(`${agent} left #${channelName}`));
34348
+ console.log(chalk2.green(`${agent} left #${spaceArg}`));
32520
34349
  } else {
32521
- console.log(chalk2.dim(`${agent} was not a member of #${channelName}`));
34350
+ console.log(chalk2.dim(`${agent} was not a member of #${spaceArg}`));
32522
34351
  }
32523
34352
  }
32524
34353
  closeDb();
32525
34354
  });
32526
- channel.command("members").description("List channel members").argument("<channel>", "Channel name").option("--json", "Output as JSON").action((channelName, opts) => {
32527
- const members = getChannelMembers(channelName);
34355
+ space.command("members").description("List space members").argument("<space>", "Space name").option("--json", "Output as JSON").action((spaceName, opts) => {
34356
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
34357
+ if (!spaceArg) {
34358
+ console.error(chalk2.red("Space name cannot be empty."));
34359
+ process.exit(1);
34360
+ }
34361
+ const members = getSpaceMembers(spaceArg);
32528
34362
  if (opts.json) {
32529
34363
  console.log(JSON.stringify(members, null, 2));
32530
34364
  } else {
32531
34365
  if (members.length === 0) {
32532
- console.log(chalk2.dim(`No members in #${channelName}.`));
34366
+ console.log(chalk2.dim(`No members in #${spaceArg}.`));
32533
34367
  } else {
32534
- console.log(chalk2.magenta(`#${channelName}`) + chalk2.dim(` \u2014 ${members.length} member(s)`));
34368
+ console.log(chalk2.magenta(`#${spaceArg}`) + chalk2.dim(` \u2014 ${members.length} member(s)`));
32535
34369
  for (const m of members) {
32536
34370
  console.log(` ${chalk2.cyan(m.agent)} ${chalk2.dim(`joined ${m.joined_at.slice(0, 10)}`)}`);
32537
34371
  }
@@ -32539,13 +34373,245 @@ channel.command("members").description("List channel members").argument("<channe
32539
34373
  }
32540
34374
  closeDb();
32541
34375
  });
34376
+ var project = program2.command("project").description("Manage projects");
34377
+ project.command("create").description("Create a new project").argument("<name>", "Project name").option("--description <text>", "Project description").option("--path <path>", "Project path on disk").option("--repository <url>", "Repository URL").option("--tags <json>", "JSON array of tags").option("--from <agent>", "Creator agent ID").option("--json", "Output as JSON").action((name, opts) => {
34378
+ const agent = resolveIdentity(opts.from).trim();
34379
+ const projectName = typeof name === "string" ? name.trim() : "";
34380
+ if (!agent) {
34381
+ console.error(chalk2.red("Creator identity is required."));
34382
+ process.exit(1);
34383
+ }
34384
+ if (!projectName) {
34385
+ console.error(chalk2.red("Project name cannot be empty."));
34386
+ process.exit(1);
34387
+ }
34388
+ let tags;
34389
+ if (opts.tags) {
34390
+ try {
34391
+ tags = JSON.parse(opts.tags);
34392
+ } catch {
34393
+ console.error(chalk2.red("Invalid --tags JSON. Expected array of strings."));
34394
+ process.exit(1);
34395
+ }
34396
+ }
34397
+ try {
34398
+ const p = createProject({
34399
+ name: projectName,
34400
+ created_by: agent,
34401
+ description: opts.description,
34402
+ path: opts.path,
34403
+ repository: opts.repository,
34404
+ tags
34405
+ });
34406
+ if (opts.json) {
34407
+ console.log(JSON.stringify(p, null, 2));
34408
+ } else {
34409
+ console.log(chalk2.green(`Project "${p.name}" created`) + chalk2.dim(` (id: ${p.id})`));
34410
+ }
34411
+ } catch (e) {
34412
+ if (e.message?.includes("UNIQUE constraint")) {
34413
+ console.error(chalk2.red(`Project "${projectName}" already exists.`));
34414
+ process.exit(1);
34415
+ }
34416
+ console.error(chalk2.red(e.message));
34417
+ process.exit(1);
34418
+ }
34419
+ closeDb();
34420
+ });
34421
+ project.command("list").description("List all projects").option("--status <status>", "Filter by status (active/archived)").option("--json", "Output as JSON").action((opts) => {
34422
+ const status = opts.status === "active" || opts.status === "archived" ? opts.status : undefined;
34423
+ const projects = listProjects(status ? { status } : undefined);
34424
+ if (opts.json) {
34425
+ console.log(JSON.stringify(projects, null, 2));
34426
+ } else {
34427
+ if (projects.length === 0) {
34428
+ console.log(chalk2.dim("No projects found."));
34429
+ } else {
34430
+ for (const p of projects) {
34431
+ const desc = p.description ? chalk2.dim(` \u2014 ${p.description}`) : "";
34432
+ const statusBadge = p.status === "archived" ? chalk2.yellow(" [archived]") : "";
34433
+ console.log(`${chalk2.bold(p.name)}${desc}${statusBadge} ${p.space_count} spaces`);
34434
+ }
34435
+ }
34436
+ }
34437
+ closeDb();
34438
+ });
34439
+ project.command("get").description("Get project details").argument("<id-or-name>", "Project ID or name").option("--json", "Output as JSON").action((idOrName, opts) => {
34440
+ let p = getProject(idOrName);
34441
+ if (!p)
34442
+ p = getProjectByName(idOrName);
34443
+ if (!p) {
34444
+ console.error(chalk2.red(`Project "${idOrName}" not found.`));
34445
+ process.exit(1);
34446
+ }
34447
+ if (opts.json) {
34448
+ console.log(JSON.stringify(p, null, 2));
34449
+ } else {
34450
+ console.log(chalk2.bold(p.name));
34451
+ if (p.description)
34452
+ console.log(` Description: ${p.description}`);
34453
+ if (p.path)
34454
+ console.log(` Path: ${p.path}`);
34455
+ if (p.repository)
34456
+ console.log(` Repository: ${p.repository}`);
34457
+ console.log(` Status: ${p.status}`);
34458
+ console.log(` Spaces: ${p.space_count}`);
34459
+ if (p.tags.length > 0)
34460
+ console.log(` Tags: ${p.tags.join(", ")}`);
34461
+ console.log(` Created by: ${p.created_by} on ${p.created_at.slice(0, 10)}`);
34462
+ }
34463
+ closeDb();
34464
+ });
34465
+ 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) => {
34466
+ const updates = {};
34467
+ if (opts.name)
34468
+ updates.name = opts.name;
34469
+ if (opts.description)
34470
+ updates.description = opts.description;
34471
+ if (opts.path)
34472
+ updates.path = opts.path;
34473
+ if (opts.status)
34474
+ updates.status = opts.status;
34475
+ if (opts.repository)
34476
+ updates.repository = opts.repository;
34477
+ if (opts.tags) {
34478
+ try {
34479
+ updates.tags = JSON.parse(opts.tags);
34480
+ } catch {
34481
+ console.error(chalk2.red("Invalid --tags JSON."));
34482
+ process.exit(1);
34483
+ }
34484
+ }
34485
+ try {
34486
+ const p = updateProject(id, updates);
34487
+ if (opts.json) {
34488
+ console.log(JSON.stringify(p, null, 2));
34489
+ } else {
34490
+ console.log(chalk2.green(`Project "${p.name}" updated.`));
34491
+ }
34492
+ } catch (e) {
34493
+ console.error(chalk2.red(e.message));
34494
+ process.exit(1);
34495
+ }
34496
+ closeDb();
34497
+ });
34498
+ project.command("delete").description("Delete a project").argument("<id>", "Project ID").option("--json", "Output as JSON").action((id, opts) => {
34499
+ try {
34500
+ const deleted = deleteProject(id);
34501
+ if (!deleted) {
34502
+ console.error(chalk2.red(`Project "${id}" not found.`));
34503
+ process.exit(1);
34504
+ }
34505
+ if (opts.json) {
34506
+ console.log(JSON.stringify({ id, deleted: true }));
34507
+ } else {
34508
+ console.log(chalk2.green(`Project deleted.`));
34509
+ }
34510
+ } catch (e) {
34511
+ console.error(chalk2.red(e.message));
34512
+ process.exit(1);
34513
+ }
34514
+ closeDb();
34515
+ });
34516
+ program2.command("delete").description("Delete a message (only sender can delete)").argument("<id>", "Message ID", parseInt).option("--from <agent>", "Sender agent ID").option("--json", "Output as JSON").action((id, opts) => {
34517
+ const agent = resolveIdentity(opts.from).trim();
34518
+ if (!agent) {
34519
+ console.error(chalk2.red("Agent identity is required."));
34520
+ process.exit(1);
34521
+ }
34522
+ const result = deleteMessage(id, agent);
34523
+ if (opts.json) {
34524
+ console.log(JSON.stringify({ id, deleted: result }));
34525
+ } else {
34526
+ if (result) {
34527
+ console.log(chalk2.green(`Message #${id} deleted.`));
34528
+ } else {
34529
+ console.error(chalk2.red(`Message #${id} not found or not your message.`));
34530
+ process.exit(1);
34531
+ }
34532
+ }
34533
+ closeDb();
34534
+ });
34535
+ program2.command("edit").description("Edit a message (only sender can edit)").argument("<id>", "Message ID", parseInt).argument("<new-content>", "New message content").option("--from <agent>", "Sender agent ID").option("--json", "Output as JSON").action((id, newContent, opts) => {
34536
+ const agent = resolveIdentity(opts.from).trim();
34537
+ const content = typeof newContent === "string" ? newContent : "";
34538
+ if (!agent) {
34539
+ console.error(chalk2.red("Agent identity is required."));
34540
+ process.exit(1);
34541
+ }
34542
+ if (!content.trim()) {
34543
+ console.error(chalk2.red("New content cannot be empty."));
34544
+ process.exit(1);
34545
+ }
34546
+ const msg = editMessage(id, agent, content);
34547
+ if (opts.json) {
34548
+ console.log(JSON.stringify(msg, null, 2));
34549
+ } else {
34550
+ if (msg) {
34551
+ console.log(chalk2.green(`Message #${id} edited.`));
34552
+ } else {
34553
+ console.error(chalk2.red(`Message #${id} not found or not your message.`));
34554
+ process.exit(1);
34555
+ }
34556
+ }
34557
+ closeDb();
34558
+ });
34559
+ program2.command("pin").description("Pin a message").argument("<id>", "Message ID", parseInt).option("--json", "Output as JSON").action((id, opts) => {
34560
+ const msg = pinMessage(id);
34561
+ if (opts.json) {
34562
+ console.log(JSON.stringify(msg, null, 2));
34563
+ } else {
34564
+ if (msg) {
34565
+ console.log(chalk2.green(`Message #${id} pinned.`));
34566
+ } else {
34567
+ console.error(chalk2.red(`Message #${id} not found.`));
34568
+ process.exit(1);
34569
+ }
34570
+ }
34571
+ closeDb();
34572
+ });
34573
+ program2.command("unpin").description("Unpin a message").argument("<id>", "Message ID", parseInt).option("--json", "Output as JSON").action((id, opts) => {
34574
+ const msg = unpinMessage(id);
34575
+ if (opts.json) {
34576
+ console.log(JSON.stringify(msg, null, 2));
34577
+ } else {
34578
+ if (msg) {
34579
+ console.log(chalk2.green(`Message #${id} unpinned.`));
34580
+ } else {
34581
+ console.error(chalk2.red(`Message #${id} not found.`));
34582
+ process.exit(1);
34583
+ }
34584
+ }
34585
+ closeDb();
34586
+ });
34587
+ program2.command("agents").description("List all agents with their presence status").option("--online", "Only show online agents").option("--json", "Output as JSON").action((opts) => {
34588
+ const agent = resolveIdentity();
34589
+ heartbeat(agent);
34590
+ const agents = listAgents({ online_only: opts.online });
34591
+ if (opts.json) {
34592
+ console.log(JSON.stringify(agents, null, 2));
34593
+ } else {
34594
+ if (agents.length === 0) {
34595
+ console.log(chalk2.dim("No agents found."));
34596
+ } else {
34597
+ for (const a of agents) {
34598
+ const status = a.online ? chalk2.green("online") : chalk2.dim("offline");
34599
+ const lastSeen = chalk2.dim(a.last_seen_at.slice(0, 19));
34600
+ const agentName = a.agent === agent ? chalk2.cyan(`${a.agent} (you)`) : chalk2.cyan(a.agent);
34601
+ console.log(` ${agentName} ${status} ${chalk2.dim(a.status)} ${lastSeen}`);
34602
+ }
34603
+ }
34604
+ }
34605
+ closeDb();
34606
+ });
32542
34607
  program2.command("mcp").description("Start MCP server").action(async () => {
32543
34608
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
32544
34609
  await startMcpServer2();
32545
34610
  });
32546
- program2.command("dashboard").description("Start web dashboard").option("--port <port>", "Port to listen on", parseInt).action(async (opts) => {
34611
+ 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) => {
32547
34612
  const { startDashboardServer: startDashboardServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
32548
- startDashboardServer2(opts.port || 3456);
34613
+ const port = Number.isFinite(opts.port) && opts.port >= 0 && opts.port <= 65535 ? opts.port : 3456;
34614
+ startDashboardServer2(port, opts.host);
32549
34615
  });
32550
34616
  program2.action(() => {
32551
34617
  const agent = resolveIdentity();