@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/dist/index.js CHANGED
@@ -1853,7 +1853,7 @@ function getDb() {
1853
1853
  session_id TEXT NOT NULL,
1854
1854
  from_agent TEXT NOT NULL,
1855
1855
  to_agent TEXT NOT NULL,
1856
- channel TEXT,
1856
+ space TEXT,
1857
1857
  content TEXT NOT NULL,
1858
1858
  priority TEXT NOT NULL DEFAULT 'normal',
1859
1859
  working_dir TEXT,
@@ -1867,27 +1867,106 @@ function getDb() {
1867
1867
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
1868
1868
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
1869
1869
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
1870
- db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
1871
- const cols = db.prepare("PRAGMA table_info(messages)").all();
1872
- if (!cols.some((c) => c.name === "channel")) {
1873
- db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
1874
- }
1870
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
1875
1871
  db.exec(`
1876
- CREATE TABLE IF NOT EXISTS channels (
1872
+ CREATE TABLE IF NOT EXISTS projects (
1873
+ id TEXT PRIMARY KEY,
1874
+ name TEXT NOT NULL UNIQUE,
1875
+ description TEXT,
1876
+ path TEXT,
1877
+ created_by TEXT NOT NULL,
1878
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1879
+ metadata TEXT,
1880
+ tags TEXT,
1881
+ status TEXT NOT NULL DEFAULT 'active',
1882
+ repository TEXT,
1883
+ settings TEXT
1884
+ )
1885
+ `);
1886
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name)");
1887
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)");
1888
+ db.exec(`
1889
+ CREATE TABLE IF NOT EXISTS spaces (
1877
1890
  name TEXT PRIMARY KEY,
1878
1891
  description TEXT,
1892
+ parent_id TEXT REFERENCES spaces(name),
1893
+ project_id TEXT REFERENCES projects(id),
1879
1894
  created_by TEXT NOT NULL,
1880
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
1895
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1896
+ archived_at TEXT
1881
1897
  )
1882
1898
  `);
1899
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_parent ON spaces(parent_id)");
1900
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_project ON spaces(project_id)");
1883
1901
  db.exec(`
1884
- CREATE TABLE IF NOT EXISTS channel_members (
1885
- channel TEXT NOT NULL REFERENCES channels(name),
1902
+ CREATE TABLE IF NOT EXISTS space_members (
1903
+ space TEXT NOT NULL REFERENCES spaces(name),
1886
1904
  agent TEXT NOT NULL,
1887
1905
  joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1888
- PRIMARY KEY (channel, agent)
1906
+ PRIMARY KEY (space, agent)
1907
+ )
1908
+ `);
1909
+ db.exec(`
1910
+ CREATE TABLE IF NOT EXISTS agent_presence (
1911
+ agent TEXT PRIMARY KEY,
1912
+ status TEXT NOT NULL DEFAULT 'online',
1913
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1914
+ metadata TEXT
1889
1915
  )
1890
1916
  `);
1917
+ const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
1918
+ const tableNames = existingTables.map((t) => t.name);
1919
+ if (tableNames.includes("channels") && tableNames.includes("spaces")) {
1920
+ const spaceCount = db.prepare("SELECT COUNT(*) as c FROM spaces").get().c;
1921
+ const channelCount = db.prepare("SELECT COUNT(*) as c FROM channels").get().c;
1922
+ if (channelCount > 0 && spaceCount === 0) {
1923
+ db.exec("BEGIN");
1924
+ try {
1925
+ db.exec(`
1926
+ INSERT OR IGNORE INTO spaces (name, description, created_by, created_at)
1927
+ SELECT name, description, created_by, created_at FROM channels
1928
+ `);
1929
+ if (tableNames.includes("channel_members")) {
1930
+ db.exec(`
1931
+ INSERT OR IGNORE INTO space_members (space, agent, joined_at)
1932
+ SELECT channel, agent, joined_at FROM channel_members
1933
+ `);
1934
+ }
1935
+ db.exec("COMMIT");
1936
+ } catch (e) {
1937
+ db.exec("ROLLBACK");
1938
+ throw e;
1939
+ }
1940
+ }
1941
+ db.exec("DROP TABLE IF EXISTS channel_members");
1942
+ db.exec("DROP TABLE IF EXISTS channels");
1943
+ }
1944
+ const msgCols = db.prepare("PRAGMA table_info(messages)").all();
1945
+ const colNames = msgCols.map((c) => c.name);
1946
+ if (colNames.includes("channel") && !colNames.includes("space")) {
1947
+ db.exec("ALTER TABLE messages ADD COLUMN space TEXT");
1948
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
1949
+ db.exec("UPDATE messages SET space = channel WHERE channel IS NOT NULL");
1950
+ db.exec(`
1951
+ UPDATE messages
1952
+ SET session_id = 'space:' || substr(session_id, 9)
1953
+ WHERE session_id LIKE 'channel:%'
1954
+ `);
1955
+ }
1956
+ const spaceCols = db.prepare("PRAGMA table_info(spaces)").all();
1957
+ const spaceColNames = spaceCols.map((c) => c.name);
1958
+ if (!spaceColNames.includes("archived_at")) {
1959
+ db.exec("ALTER TABLE spaces ADD COLUMN archived_at TEXT");
1960
+ }
1961
+ const msgCols2 = db.prepare("PRAGMA table_info(messages)").all();
1962
+ const colNames2 = msgCols2.map((c) => c.name);
1963
+ if (!colNames2.includes("edited_at")) {
1964
+ db.exec("ALTER TABLE messages ADD COLUMN edited_at TEXT");
1965
+ }
1966
+ if (!colNames2.includes("pinned_at")) {
1967
+ db.exec("ALTER TABLE messages ADD COLUMN pinned_at TEXT");
1968
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_pinned ON messages(pinned_at)");
1969
+ }
1891
1970
  return db;
1892
1971
  }
1893
1972
  function closeDb() {
@@ -1900,21 +1979,31 @@ function closeDb() {
1900
1979
  // src/lib/messages.ts
1901
1980
  import { randomUUID } from "crypto";
1902
1981
  function parseMessage(row) {
1982
+ let metadata = null;
1983
+ if (row.metadata) {
1984
+ try {
1985
+ metadata = JSON.parse(row.metadata);
1986
+ } catch {
1987
+ metadata = null;
1988
+ }
1989
+ }
1903
1990
  return {
1904
1991
  ...row,
1905
- metadata: row.metadata ? JSON.parse(row.metadata) : null
1992
+ metadata
1906
1993
  };
1907
1994
  }
1908
1995
  function sendMessage(opts) {
1909
1996
  const db2 = getDb();
1910
- const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
1997
+ const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
1998
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`);
1911
1999
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
2000
+ const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
1912
2001
  const stmt = db2.prepare(`
1913
- INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
2002
+ INSERT INTO messages (session_id, from_agent, to_agent, space, content, priority, working_dir, repository, branch, metadata)
1914
2003
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1915
2004
  RETURNING *
1916
2005
  `);
1917
- 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);
2006
+ 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);
1918
2007
  return parseMessage(row);
1919
2008
  }
1920
2009
  function readMessages(opts = {}) {
@@ -1933,20 +2022,25 @@ function readMessages(opts = {}) {
1933
2022
  conditions.push("to_agent = ?");
1934
2023
  params.push(opts.to);
1935
2024
  }
1936
- if (opts.channel) {
1937
- conditions.push("channel = ?");
1938
- params.push(opts.channel);
2025
+ if (opts.space) {
2026
+ conditions.push("space = ?");
2027
+ params.push(opts.space);
1939
2028
  }
1940
2029
  if (opts.since) {
1941
2030
  conditions.push("created_at > ?");
1942
2031
  params.push(opts.since);
1943
2032
  }
2033
+ if (opts.since_id !== undefined) {
2034
+ conditions.push("id > ?");
2035
+ params.push(opts.since_id);
2036
+ }
1944
2037
  if (opts.unread_only) {
1945
2038
  conditions.push("read_at IS NULL");
1946
2039
  }
1947
2040
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1948
- const limit = opts.limit ? `LIMIT ${opts.limit}` : "";
1949
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC ${limit}`).all(...params);
2041
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
2042
+ const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
2043
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} ${limit}`).all(...params);
1950
2044
  return rows.map(parseMessage);
1951
2045
  }
1952
2046
  function markRead(ids, reader) {
@@ -1964,10 +2058,10 @@ function markSessionRead(sessionId, reader) {
1964
2058
  const result = stmt.run(sessionId, reader);
1965
2059
  return result.changes;
1966
2060
  }
1967
- function markChannelRead(channelName, reader) {
2061
+ function markSpaceRead(spaceName, reader) {
1968
2062
  const db2 = getDb();
1969
- 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`);
1970
- const result = stmt.run(channelName, reader);
2063
+ 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`);
2064
+ const result = stmt.run(spaceName, reader);
1971
2065
  return result.changes;
1972
2066
  }
1973
2067
  function getMessageById(id) {
@@ -1975,6 +2069,130 @@ function getMessageById(id) {
1975
2069
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
1976
2070
  return row ? parseMessage(row) : null;
1977
2071
  }
2072
+ function markAllRead(agent) {
2073
+ const db2 = getDb();
2074
+ 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`);
2075
+ const result = stmt.run(agent);
2076
+ return result.changes;
2077
+ }
2078
+ function escapeCsvField(value) {
2079
+ if (value === null || value === undefined)
2080
+ return "";
2081
+ const str = String(value);
2082
+ if (str.includes(",") || str.includes('"') || str.includes(`
2083
+ `) || str.includes("\r")) {
2084
+ return `"${str.replace(/"/g, '""')}"`;
2085
+ }
2086
+ return str;
2087
+ }
2088
+ function exportMessages(opts) {
2089
+ const db2 = getDb();
2090
+ const conditions = [];
2091
+ const params = [];
2092
+ if (opts?.space) {
2093
+ conditions.push("space = ?");
2094
+ params.push(opts.space);
2095
+ }
2096
+ if (opts?.session_id) {
2097
+ conditions.push("session_id = ?");
2098
+ params.push(opts.session_id);
2099
+ }
2100
+ if (opts?.from) {
2101
+ conditions.push("from_agent = ?");
2102
+ params.push(opts.from);
2103
+ }
2104
+ if (opts?.since) {
2105
+ conditions.push("created_at >= ?");
2106
+ params.push(opts.since);
2107
+ }
2108
+ if (opts?.until) {
2109
+ conditions.push("created_at <= ?");
2110
+ params.push(opts.until);
2111
+ }
2112
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2113
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC, id ASC`).all(...params);
2114
+ const messages = rows.map(parseMessage);
2115
+ const format = opts?.format ?? "json";
2116
+ if (format === "csv") {
2117
+ const headers = "id,session_id,from_agent,to_agent,space,content,priority,created_at,read_at";
2118
+ const lines = messages.map((m) => [
2119
+ String(m.id),
2120
+ escapeCsvField(m.session_id),
2121
+ escapeCsvField(m.from_agent),
2122
+ escapeCsvField(m.to_agent),
2123
+ escapeCsvField(m.space),
2124
+ escapeCsvField(m.content),
2125
+ escapeCsvField(m.priority),
2126
+ escapeCsvField(m.created_at),
2127
+ escapeCsvField(m.read_at)
2128
+ ].join(","));
2129
+ return [headers, ...lines].join(`
2130
+ `);
2131
+ }
2132
+ return JSON.stringify(messages, null, 2);
2133
+ }
2134
+ function deleteMessage(id, agent) {
2135
+ const db2 = getDb();
2136
+ const stmt = db2.prepare("DELETE FROM messages WHERE id = ? AND from_agent = ?");
2137
+ const result = stmt.run(id, agent);
2138
+ return result.changes > 0;
2139
+ }
2140
+ function editMessage(id, agent, newContent) {
2141
+ const db2 = getDb();
2142
+ const stmt = db2.prepare(`UPDATE messages SET content = ?, edited_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? AND from_agent = ? RETURNING *`);
2143
+ const row = stmt.get(newContent, id, agent);
2144
+ return row ? parseMessage(row) : null;
2145
+ }
2146
+ function pinMessage(id) {
2147
+ const db2 = getDb();
2148
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? RETURNING *`);
2149
+ const row = stmt.get(id);
2150
+ return row ? parseMessage(row) : null;
2151
+ }
2152
+ function unpinMessage(id) {
2153
+ const db2 = getDb();
2154
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = NULL WHERE id = ? RETURNING *`);
2155
+ const row = stmt.get(id);
2156
+ return row ? parseMessage(row) : null;
2157
+ }
2158
+ function getPinnedMessages(opts) {
2159
+ const db2 = getDb();
2160
+ const conditions = ["pinned_at IS NOT NULL"];
2161
+ const params = [];
2162
+ if (opts?.space) {
2163
+ conditions.push("space = ?");
2164
+ params.push(opts.space);
2165
+ }
2166
+ if (opts?.session_id) {
2167
+ conditions.push("session_id = ?");
2168
+ params.push(opts.session_id);
2169
+ }
2170
+ const where = `WHERE ${conditions.join(" AND ")}`;
2171
+ const limit = Number.isFinite(opts?.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
2172
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limit}`).all(...params);
2173
+ return rows.map(parseMessage);
2174
+ }
2175
+ function searchMessages(opts) {
2176
+ const db2 = getDb();
2177
+ const conditions = ["content LIKE ?"];
2178
+ const params = [`%${opts.query}%`];
2179
+ if (opts.space) {
2180
+ conditions.push("space = ?");
2181
+ params.push(opts.space);
2182
+ }
2183
+ if (opts.from) {
2184
+ conditions.push("from_agent = ?");
2185
+ params.push(opts.from);
2186
+ }
2187
+ if (opts.to) {
2188
+ conditions.push("to_agent = ?");
2189
+ params.push(opts.to);
2190
+ }
2191
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
2192
+ const where = `WHERE ${conditions.join(" AND ")}`;
2193
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
2194
+ return rows.map(parseMessage);
2195
+ }
1978
2196
  // src/lib/sessions.ts
1979
2197
  function listSessions(agent) {
1980
2198
  const db2 = getDb();
@@ -2029,85 +2247,395 @@ function getSession(sessionId) {
2029
2247
  unread_count: row.unread_count
2030
2248
  };
2031
2249
  }
2032
- // src/lib/channels.ts
2033
- function createChannel(name, createdBy, description) {
2250
+ // src/lib/spaces.ts
2251
+ function getSpaceDepth(spaceName) {
2034
2252
  const db2 = getDb();
2035
- const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
2036
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
2253
+ let depth = 0;
2254
+ let current = spaceName;
2255
+ for (let i = 0;i < 10; i++) {
2256
+ const row = db2.prepare("SELECT parent_id FROM spaces WHERE name = ?").get(current);
2257
+ if (!row || !row.parent_id)
2258
+ break;
2259
+ depth++;
2260
+ current = row.parent_id;
2261
+ }
2262
+ return depth;
2263
+ }
2264
+ function createSpace(name, createdBy, options) {
2265
+ const db2 = getDb();
2266
+ if (options?.parent_id) {
2267
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(options.parent_id);
2268
+ if (!parentExists) {
2269
+ throw new Error(`Parent space not found: ${options.parent_id}`);
2270
+ }
2271
+ const parentDepth = getSpaceDepth(options.parent_id);
2272
+ if (parentDepth >= 2) {
2273
+ throw new Error("Maximum space nesting depth is 3 levels");
2274
+ }
2275
+ }
2276
+ if (options?.project_id) {
2277
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(options.project_id);
2278
+ if (!projectExists) {
2279
+ throw new Error(`Project not found: ${options.project_id}`);
2280
+ }
2281
+ }
2282
+ 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);
2283
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(name, createdBy);
2037
2284
  return row;
2038
2285
  }
2039
- function listChannels() {
2286
+ function listSpaces(options) {
2040
2287
  const db2 = getDb();
2288
+ const conditions = [];
2289
+ const params = [];
2290
+ if (options?.project_id) {
2291
+ conditions.push("s.project_id = ?");
2292
+ params.push(options.project_id);
2293
+ }
2294
+ if (options?.parent_id !== undefined) {
2295
+ if (options.parent_id === null) {
2296
+ conditions.push("s.parent_id IS NULL");
2297
+ } else {
2298
+ conditions.push("s.parent_id = ?");
2299
+ params.push(options.parent_id);
2300
+ }
2301
+ }
2302
+ if (!options?.include_archived) {
2303
+ conditions.push("s.archived_at IS NULL");
2304
+ }
2305
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2041
2306
  const rows = db2.prepare(`
2042
2307
  SELECT
2043
- c.name,
2044
- c.description,
2045
- c.created_by,
2046
- c.created_at,
2047
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2048
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2049
- FROM channels c
2050
- ORDER BY c.name ASC
2051
- `).all();
2308
+ s.name,
2309
+ s.description,
2310
+ s.parent_id,
2311
+ s.project_id,
2312
+ s.created_by,
2313
+ s.created_at,
2314
+ s.archived_at,
2315
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
2316
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
2317
+ FROM spaces s
2318
+ ${where}
2319
+ ORDER BY s.name ASC
2320
+ `).all(...params);
2052
2321
  return rows;
2053
2322
  }
2054
- function getChannel(name) {
2323
+ function getSpace(name) {
2055
2324
  const db2 = getDb();
2056
2325
  const row = db2.prepare(`
2057
2326
  SELECT
2058
- c.name,
2059
- c.description,
2060
- c.created_by,
2061
- c.created_at,
2062
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
2063
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
2064
- FROM channels c
2065
- WHERE c.name = ?
2327
+ s.name,
2328
+ s.description,
2329
+ s.parent_id,
2330
+ s.project_id,
2331
+ s.created_by,
2332
+ s.created_at,
2333
+ s.archived_at,
2334
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
2335
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
2336
+ FROM spaces s
2337
+ WHERE s.name = ?
2066
2338
  `).get(name);
2067
2339
  return row;
2068
2340
  }
2069
- function joinChannel(channelName, agent) {
2341
+ function joinSpace(spaceName, agent) {
2070
2342
  const db2 = getDb();
2071
- const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
2072
- if (!channel)
2343
+ const space = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(spaceName);
2344
+ if (!space)
2073
2345
  return false;
2074
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
2346
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(spaceName, agent);
2075
2347
  return true;
2076
2348
  }
2077
- function leaveChannel(channelName, agent) {
2349
+ function leaveSpace(spaceName, agent) {
2078
2350
  const db2 = getDb();
2079
- const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
2351
+ const result = db2.prepare("DELETE FROM space_members WHERE space = ? AND agent = ?").run(spaceName, agent);
2080
2352
  return result.changes > 0;
2081
2353
  }
2082
- function getChannelMembers(channelName) {
2354
+ function getSpaceMembers(spaceName) {
2355
+ const db2 = getDb();
2356
+ return db2.prepare("SELECT space, agent, joined_at FROM space_members WHERE space = ? ORDER BY joined_at ASC").all(spaceName);
2357
+ }
2358
+ function updateSpace(name, updates) {
2359
+ const db2 = getDb();
2360
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2361
+ if (!existing) {
2362
+ throw new Error(`Space not found: ${name}`);
2363
+ }
2364
+ if (updates.parent_id !== undefined && updates.parent_id !== existing.parent_id) {
2365
+ if (updates.parent_id !== null) {
2366
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(updates.parent_id);
2367
+ if (!parentExists) {
2368
+ throw new Error(`Parent space not found: ${updates.parent_id}`);
2369
+ }
2370
+ const parentDepth = getSpaceDepth(updates.parent_id);
2371
+ if (parentDepth >= 2) {
2372
+ throw new Error("Maximum space nesting depth is 3 levels");
2373
+ }
2374
+ if (updates.parent_id === name) {
2375
+ throw new Error("A space cannot be its own parent");
2376
+ }
2377
+ }
2378
+ }
2379
+ if (updates.project_id !== undefined && updates.project_id !== existing.project_id) {
2380
+ if (updates.project_id !== null) {
2381
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(updates.project_id);
2382
+ if (!projectExists) {
2383
+ throw new Error(`Project not found: ${updates.project_id}`);
2384
+ }
2385
+ }
2386
+ }
2387
+ const sets = [];
2388
+ const params = [];
2389
+ if (updates.description !== undefined) {
2390
+ sets.push("description = ?");
2391
+ params.push(updates.description);
2392
+ }
2393
+ if (updates.parent_id !== undefined) {
2394
+ sets.push("parent_id = ?");
2395
+ params.push(updates.parent_id);
2396
+ }
2397
+ if (updates.project_id !== undefined) {
2398
+ sets.push("project_id = ?");
2399
+ params.push(updates.project_id);
2400
+ }
2401
+ if (sets.length === 0) {
2402
+ return existing;
2403
+ }
2404
+ params.push(name);
2405
+ const row = db2.prepare(`UPDATE spaces SET ${sets.join(", ")} WHERE name = ? RETURNING *`).get(...params);
2406
+ return row;
2407
+ }
2408
+ function archiveSpace(name) {
2083
2409
  const db2 = getDb();
2084
- return db2.prepare("SELECT channel, agent, joined_at FROM channel_members WHERE channel = ? ORDER BY joined_at ASC").all(channelName);
2410
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2411
+ if (!existing) {
2412
+ throw new Error(`Space not found: ${name}`);
2413
+ }
2414
+ const row = db2.prepare("UPDATE spaces SET archived_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE name = ? RETURNING *").get(name);
2415
+ return row;
2085
2416
  }
2086
- function isChannelMember(channelName, agent) {
2417
+ function unarchiveSpace(name) {
2087
2418
  const db2 = getDb();
2088
- const row = db2.prepare("SELECT 1 FROM channel_members WHERE channel = ? AND agent = ?").get(channelName, agent);
2419
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
2420
+ if (!existing) {
2421
+ throw new Error(`Space not found: ${name}`);
2422
+ }
2423
+ const row = db2.prepare("UPDATE spaces SET archived_at = NULL WHERE name = ? RETURNING *").get(name);
2424
+ return row;
2425
+ }
2426
+ function isSpaceMember(spaceName, agent) {
2427
+ const db2 = getDb();
2428
+ const row = db2.prepare("SELECT 1 FROM space_members WHERE space = ? AND agent = ?").get(spaceName, agent);
2089
2429
  return !!row;
2090
2430
  }
2431
+ // src/lib/projects.ts
2432
+ import { randomUUID as randomUUID2 } from "crypto";
2433
+ function parseProject(row) {
2434
+ let metadata = null;
2435
+ if (row.metadata) {
2436
+ try {
2437
+ metadata = JSON.parse(row.metadata);
2438
+ } catch {
2439
+ metadata = null;
2440
+ }
2441
+ }
2442
+ let tags = [];
2443
+ if (row.tags) {
2444
+ try {
2445
+ tags = JSON.parse(row.tags);
2446
+ } catch {
2447
+ tags = [];
2448
+ }
2449
+ }
2450
+ let settings = null;
2451
+ if (row.settings) {
2452
+ try {
2453
+ settings = JSON.parse(row.settings);
2454
+ } catch {
2455
+ settings = null;
2456
+ }
2457
+ }
2458
+ return {
2459
+ id: row.id,
2460
+ name: row.name,
2461
+ description: row.description || null,
2462
+ path: row.path || null,
2463
+ created_by: row.created_by,
2464
+ created_at: row.created_at,
2465
+ metadata,
2466
+ tags,
2467
+ status: row.status || "active",
2468
+ repository: row.repository || null,
2469
+ settings
2470
+ };
2471
+ }
2472
+ function createProject(opts) {
2473
+ const db2 = getDb();
2474
+ const id = randomUUID2();
2475
+ const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
2476
+ const tags = opts.tags ? JSON.stringify(opts.tags) : null;
2477
+ const settings = opts.settings ? JSON.stringify(opts.settings) : null;
2478
+ const row = db2.prepare(`
2479
+ INSERT INTO projects (id, name, description, path, created_by, metadata, tags, repository, settings)
2480
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2481
+ RETURNING *
2482
+ `).get(id, opts.name, opts.description || null, opts.path || null, opts.created_by, metadata, tags, opts.repository || null, settings);
2483
+ return parseProject(row);
2484
+ }
2485
+ function listProjects(opts) {
2486
+ const db2 = getDb();
2487
+ const conditions = [];
2488
+ const params = [];
2489
+ if (opts?.status) {
2490
+ conditions.push("p.status = ?");
2491
+ params.push(opts.status);
2492
+ }
2493
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2494
+ const rows = db2.prepare(`
2495
+ SELECT
2496
+ p.*,
2497
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2498
+ FROM projects p
2499
+ ${where}
2500
+ ORDER BY p.name ASC
2501
+ `).all(...params);
2502
+ return rows.map((row) => ({
2503
+ ...parseProject(row),
2504
+ space_count: row.space_count
2505
+ }));
2506
+ }
2507
+ function getProject(id) {
2508
+ const db2 = getDb();
2509
+ const row = db2.prepare(`
2510
+ SELECT
2511
+ p.*,
2512
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2513
+ FROM projects p
2514
+ WHERE p.id = ?
2515
+ `).get(id);
2516
+ if (!row)
2517
+ return null;
2518
+ return {
2519
+ ...parseProject(row),
2520
+ space_count: row.space_count
2521
+ };
2522
+ }
2523
+ function getProjectByName(name) {
2524
+ const db2 = getDb();
2525
+ const row = db2.prepare(`
2526
+ SELECT
2527
+ p.*,
2528
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
2529
+ FROM projects p
2530
+ WHERE p.name = ?
2531
+ `).get(name);
2532
+ if (!row)
2533
+ return null;
2534
+ return {
2535
+ ...parseProject(row),
2536
+ space_count: row.space_count
2537
+ };
2538
+ }
2539
+ function updateProject(id, updates) {
2540
+ const db2 = getDb();
2541
+ const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(id);
2542
+ if (!existing) {
2543
+ throw new Error(`Project not found: ${id}`);
2544
+ }
2545
+ const sets = [];
2546
+ const params = [];
2547
+ if (updates.name !== undefined) {
2548
+ sets.push("name = ?");
2549
+ params.push(updates.name);
2550
+ }
2551
+ if (updates.description !== undefined) {
2552
+ sets.push("description = ?");
2553
+ params.push(updates.description);
2554
+ }
2555
+ if (updates.path !== undefined) {
2556
+ sets.push("path = ?");
2557
+ params.push(updates.path);
2558
+ }
2559
+ if (updates.metadata !== undefined) {
2560
+ sets.push("metadata = ?");
2561
+ params.push(JSON.stringify(updates.metadata));
2562
+ }
2563
+ if (updates.tags !== undefined) {
2564
+ sets.push("tags = ?");
2565
+ params.push(JSON.stringify(updates.tags));
2566
+ }
2567
+ if (updates.status !== undefined) {
2568
+ sets.push("status = ?");
2569
+ params.push(updates.status);
2570
+ }
2571
+ if (updates.repository !== undefined) {
2572
+ sets.push("repository = ?");
2573
+ params.push(updates.repository);
2574
+ }
2575
+ if (updates.settings !== undefined) {
2576
+ sets.push("settings = ?");
2577
+ params.push(JSON.stringify(updates.settings));
2578
+ }
2579
+ if (sets.length === 0) {
2580
+ return parseProject(existing);
2581
+ }
2582
+ params.push(id);
2583
+ const row = db2.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
2584
+ return parseProject(row);
2585
+ }
2586
+ function deleteProject(id) {
2587
+ const db2 = getDb();
2588
+ const spaceCount = db2.prepare("SELECT COUNT(*) as c FROM spaces WHERE project_id = ?").get(id).c;
2589
+ if (spaceCount > 0) {
2590
+ throw new Error(`Cannot delete project: ${spaceCount} space(s) still reference it`);
2591
+ }
2592
+ const result = db2.prepare("DELETE FROM projects WHERE id = ?").run(id);
2593
+ return result.changes > 0;
2594
+ }
2091
2595
  // src/lib/poll.ts
2092
2596
  var import_react = __toESM(require_react(), 1);
2093
2597
  function startPolling(opts) {
2094
2598
  const interval = opts.interval_ms ?? 200;
2095
- let lastSeen = new Date().toISOString();
2096
2599
  let stopped = false;
2097
- const poll = () => {
2098
- if (stopped)
2099
- return;
2100
- const messages = readMessages({
2600
+ let inFlight = false;
2601
+ let lastSeenId = 0;
2602
+ const seedLastSeen = () => {
2603
+ const latest = readMessages({
2101
2604
  session_id: opts.session_id,
2102
2605
  to: opts.to_agent,
2103
- channel: opts.channel,
2104
- since: lastSeen
2606
+ space: opts.space,
2607
+ order: "desc",
2608
+ limit: 1
2105
2609
  });
2106
- if (messages.length > 0) {
2107
- lastSeen = messages[messages.length - 1].created_at;
2108
- opts.on_messages(messages);
2610
+ if (latest.length > 0) {
2611
+ lastSeenId = latest[0].id;
2109
2612
  }
2110
2613
  };
2614
+ const poll = () => {
2615
+ if (stopped || inFlight)
2616
+ return;
2617
+ inFlight = true;
2618
+ try {
2619
+ const messages = readMessages({
2620
+ session_id: opts.session_id,
2621
+ to: opts.to_agent,
2622
+ space: opts.space,
2623
+ since_id: lastSeenId,
2624
+ order: "asc"
2625
+ });
2626
+ if (messages.length > 0) {
2627
+ lastSeenId = messages[messages.length - 1].id;
2628
+ try {
2629
+ opts.on_messages(messages);
2630
+ } catch (error) {
2631
+ console.error("Polling callback error:", error);
2632
+ }
2633
+ }
2634
+ } finally {
2635
+ inFlight = false;
2636
+ }
2637
+ };
2638
+ seedLastSeen();
2111
2639
  const timer = setInterval(poll, interval);
2112
2640
  return {
2113
2641
  stop: () => {
@@ -2116,62 +2644,141 @@ function startPolling(opts) {
2116
2644
  }
2117
2645
  };
2118
2646
  }
2119
- function useChannelMessages(channelName) {
2647
+ function useSpaceMessages(spaceName) {
2120
2648
  const [messages, setMessages] = import_react.useState([]);
2121
- const initialLoad = import_react.useRef(false);
2122
2649
  import_react.useEffect(() => {
2123
- if (!initialLoad.current) {
2124
- const existing = readMessages({ channel: channelName });
2125
- setMessages(existing);
2126
- initialLoad.current = true;
2127
- }
2650
+ const existing = readMessages({ space: spaceName });
2651
+ setMessages(existing);
2128
2652
  const { stop } = startPolling({
2129
- channel: channelName,
2653
+ space: spaceName,
2130
2654
  interval_ms: 200,
2131
2655
  on_messages: (newMessages) => {
2132
2656
  setMessages((prev) => [...prev, ...newMessages]);
2133
2657
  }
2134
2658
  });
2135
2659
  return stop;
2136
- }, [channelName]);
2660
+ }, [spaceName]);
2137
2661
  return messages;
2138
2662
  }
2139
2663
  // src/lib/identity.ts
2140
2664
  function resolveIdentity(explicit) {
2141
- if (explicit)
2142
- return explicit;
2143
- if (process.env.CONVERSATIONS_AGENT_ID)
2144
- return process.env.CONVERSATIONS_AGENT_ID;
2665
+ const explicitValue = explicit?.trim();
2666
+ if (explicitValue)
2667
+ return explicitValue;
2668
+ const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
2669
+ if (envValue)
2670
+ return envValue;
2145
2671
  return "user";
2146
2672
  }
2147
2673
  function requireIdentity(explicit) {
2148
- if (explicit)
2149
- return explicit;
2150
- if (process.env.CONVERSATIONS_AGENT_ID)
2151
- return process.env.CONVERSATIONS_AGENT_ID;
2674
+ const explicitValue = explicit?.trim();
2675
+ if (explicitValue)
2676
+ return explicitValue;
2677
+ const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
2678
+ if (envValue)
2679
+ return envValue;
2152
2680
  throw new Error("Agent identity required. Set CONVERSATIONS_AGENT_ID env var or pass --from flag.");
2153
2681
  }
2682
+ // src/lib/presence.ts
2683
+ var ONLINE_THRESHOLD_SECONDS = 60;
2684
+ function parsePresence(row) {
2685
+ let metadata = null;
2686
+ if (row.metadata) {
2687
+ try {
2688
+ metadata = JSON.parse(row.metadata);
2689
+ } catch {
2690
+ metadata = null;
2691
+ }
2692
+ }
2693
+ const lastSeenAt = row.last_seen_at;
2694
+ const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
2695
+ const nowMs = Date.now();
2696
+ const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
2697
+ return {
2698
+ agent: row.agent,
2699
+ status: row.status,
2700
+ last_seen_at: lastSeenAt,
2701
+ online,
2702
+ metadata
2703
+ };
2704
+ }
2705
+ function heartbeat(agent, status, metadata) {
2706
+ const db2 = getDb();
2707
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
2708
+ const resolvedStatus = status || "online";
2709
+ db2.prepare(`
2710
+ INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
2711
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
2712
+ ON CONFLICT(agent) DO UPDATE SET
2713
+ status = excluded.status,
2714
+ last_seen_at = excluded.last_seen_at,
2715
+ metadata = excluded.metadata
2716
+ `).run(agent, resolvedStatus, metadataJson);
2717
+ }
2718
+ function getPresence(agent) {
2719
+ const db2 = getDb();
2720
+ const row = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(agent);
2721
+ return row ? parsePresence(row) : null;
2722
+ }
2723
+ function listAgents(opts) {
2724
+ const db2 = getDb();
2725
+ let query = "SELECT * FROM agent_presence";
2726
+ const params = [];
2727
+ if (opts?.online_only) {
2728
+ query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
2729
+ }
2730
+ query += " ORDER BY last_seen_at DESC";
2731
+ const rows = db2.prepare(query).all(...params);
2732
+ return rows.map(parsePresence);
2733
+ }
2734
+ function removePresence(agent) {
2735
+ const db2 = getDb();
2736
+ const result = db2.prepare("DELETE FROM agent_presence WHERE agent = ?").run(agent);
2737
+ return result.changes > 0;
2738
+ }
2154
2739
  export {
2155
- useChannelMessages,
2740
+ useSpaceMessages,
2741
+ updateSpace,
2742
+ updateProject,
2743
+ unpinMessage,
2744
+ unarchiveSpace,
2156
2745
  startPolling,
2157
2746
  sendMessage,
2747
+ searchMessages,
2158
2748
  resolveIdentity,
2159
2749
  requireIdentity,
2750
+ removePresence,
2160
2751
  readMessages,
2752
+ pinMessage,
2753
+ markSpaceRead,
2161
2754
  markSessionRead,
2162
2755
  markRead,
2163
- markChannelRead,
2756
+ markAllRead,
2757
+ listSpaces,
2164
2758
  listSessions,
2165
- listChannels,
2166
- leaveChannel,
2167
- joinChannel,
2168
- isChannelMember,
2759
+ listProjects,
2760
+ listAgents,
2761
+ leaveSpace,
2762
+ joinSpace,
2763
+ isSpaceMember,
2764
+ heartbeat,
2765
+ getSpaceMembers,
2766
+ getSpaceDepth,
2767
+ getSpace,
2169
2768
  getSession,
2769
+ getProjectByName,
2770
+ getProject,
2771
+ getPresence,
2772
+ getPinnedMessages,
2170
2773
  getMessageById,
2171
2774
  getDbPath,
2172
2775
  getDb,
2173
- getChannelMembers,
2174
- getChannel,
2175
- createChannel,
2176
- closeDb
2776
+ exportMessages,
2777
+ editMessage,
2778
+ deleteProject,
2779
+ deleteMessage,
2780
+ createSpace,
2781
+ createProject,
2782
+ closeDb,
2783
+ archiveSpace
2177
2784
  };