@hasna/conversations 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hook.js +23 -0
- package/bin/index.js +100 -71
- package/bin/mcp.js +97 -64
- package/dashboard/dist/assets/index-Bw0wMcXE.js +186 -0
- package/dashboard/dist/assets/index-CF_GDtNp.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +84 -6
- package/dist/lib/presence.d.ts +4 -2
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
package/bin/hook.js
CHANGED
|
@@ -114,9 +114,13 @@ function getDb() {
|
|
|
114
114
|
`);
|
|
115
115
|
db.exec(`
|
|
116
116
|
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
117
|
+
id TEXT NOT NULL,
|
|
117
118
|
agent TEXT PRIMARY KEY,
|
|
119
|
+
session_id TEXT,
|
|
120
|
+
role TEXT NOT NULL DEFAULT 'agent',
|
|
118
121
|
status TEXT NOT NULL DEFAULT 'online',
|
|
119
122
|
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
123
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
120
124
|
metadata TEXT
|
|
121
125
|
)
|
|
122
126
|
`);
|
|
@@ -195,6 +199,25 @@ function getDb() {
|
|
|
195
199
|
db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
|
|
196
200
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
|
|
197
201
|
}
|
|
202
|
+
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
203
|
+
const presenceColNames = presenceCols.map((c) => c.name);
|
|
204
|
+
if (!presenceColNames.includes("id")) {
|
|
205
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
|
|
206
|
+
const rows = db.prepare("SELECT agent FROM agent_presence").all();
|
|
207
|
+
for (const row of rows) {
|
|
208
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
209
|
+
db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!presenceColNames.includes("session_id")) {
|
|
213
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
|
|
214
|
+
}
|
|
215
|
+
if (!presenceColNames.includes("role")) {
|
|
216
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
|
|
217
|
+
}
|
|
218
|
+
if (!presenceColNames.includes("created_at")) {
|
|
219
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
|
|
220
|
+
}
|
|
198
221
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
199
222
|
if (!ftsExists) {
|
|
200
223
|
db.exec(`
|
package/bin/index.js
CHANGED
|
@@ -1968,9 +1968,13 @@ function getDb() {
|
|
|
1968
1968
|
`);
|
|
1969
1969
|
db.exec(`
|
|
1970
1970
|
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
1971
|
+
id TEXT NOT NULL,
|
|
1971
1972
|
agent TEXT PRIMARY KEY,
|
|
1973
|
+
session_id TEXT,
|
|
1974
|
+
role TEXT NOT NULL DEFAULT 'agent',
|
|
1972
1975
|
status TEXT NOT NULL DEFAULT 'online',
|
|
1973
1976
|
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
1977
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
1974
1978
|
metadata TEXT
|
|
1975
1979
|
)
|
|
1976
1980
|
`);
|
|
@@ -2049,6 +2053,25 @@ function getDb() {
|
|
|
2049
2053
|
db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
|
|
2050
2054
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
|
|
2051
2055
|
}
|
|
2056
|
+
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
2057
|
+
const presenceColNames = presenceCols.map((c) => c.name);
|
|
2058
|
+
if (!presenceColNames.includes("id")) {
|
|
2059
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
|
|
2060
|
+
const rows = db.prepare("SELECT agent FROM agent_presence").all();
|
|
2061
|
+
for (const row of rows) {
|
|
2062
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
2063
|
+
db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (!presenceColNames.includes("session_id")) {
|
|
2067
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
|
|
2068
|
+
}
|
|
2069
|
+
if (!presenceColNames.includes("role")) {
|
|
2070
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
|
|
2071
|
+
}
|
|
2072
|
+
if (!presenceColNames.includes("created_at")) {
|
|
2073
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
|
|
2074
|
+
}
|
|
2052
2075
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
2053
2076
|
if (!ftsExists) {
|
|
2054
2077
|
db.exec(`
|
|
@@ -3311,26 +3334,76 @@ function parsePresence(row) {
|
|
|
3311
3334
|
const nowMs = Date.now();
|
|
3312
3335
|
const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
|
|
3313
3336
|
return {
|
|
3337
|
+
id: row.id || "",
|
|
3314
3338
|
agent: row.agent,
|
|
3339
|
+
session_id: row.session_id ?? null,
|
|
3340
|
+
role: row.role || "agent",
|
|
3315
3341
|
status: row.status,
|
|
3316
3342
|
last_seen_at: lastSeenAt,
|
|
3343
|
+
created_at: row.created_at || lastSeenAt,
|
|
3317
3344
|
online,
|
|
3318
3345
|
metadata
|
|
3319
3346
|
};
|
|
3320
3347
|
}
|
|
3321
|
-
function
|
|
3348
|
+
function isActiveSession(lastSeenAt) {
|
|
3349
|
+
const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
|
|
3350
|
+
const nowMs = Date.now();
|
|
3351
|
+
return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
|
|
3352
|
+
}
|
|
3353
|
+
function registerAgent(name, sessionId, role) {
|
|
3354
|
+
const db2 = getDb();
|
|
3355
|
+
const normalizedName = name.trim().toLowerCase();
|
|
3356
|
+
const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
3357
|
+
if (existing) {
|
|
3358
|
+
const lastSeenAt = existing.last_seen_at;
|
|
3359
|
+
const existingSessionId = existing.session_id;
|
|
3360
|
+
if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
|
|
3361
|
+
return {
|
|
3362
|
+
conflict: true,
|
|
3363
|
+
error: "agent_conflict",
|
|
3364
|
+
message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
|
|
3365
|
+
existing_id: existing.id,
|
|
3366
|
+
existing_name: normalizedName,
|
|
3367
|
+
existing_session_id: existingSessionId,
|
|
3368
|
+
last_seen_at: lastSeenAt,
|
|
3369
|
+
session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
|
|
3370
|
+
working_dir: null
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
const tookOver = existingSessionId !== sessionId;
|
|
3374
|
+
db2.prepare(`
|
|
3375
|
+
UPDATE agent_presence
|
|
3376
|
+
SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
3377
|
+
WHERE agent = ?
|
|
3378
|
+
`).run(sessionId, role || existing.role || "agent", normalizedName);
|
|
3379
|
+
const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
3380
|
+
return { agent: parsePresence(updated), created: false, took_over: tookOver };
|
|
3381
|
+
}
|
|
3382
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
3383
|
+
const resolvedRole = role || "agent";
|
|
3384
|
+
db2.prepare(`
|
|
3385
|
+
INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
|
|
3386
|
+
VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
3387
|
+
`).run(id, normalizedName, sessionId, resolvedRole);
|
|
3388
|
+
const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
3389
|
+
return { agent: parsePresence(created), created: true, took_over: false };
|
|
3390
|
+
}
|
|
3391
|
+
function heartbeat(agent, status, metadata, sessionId) {
|
|
3322
3392
|
const db2 = getDb();
|
|
3323
3393
|
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
3324
3394
|
const resolvedStatus = status || "online";
|
|
3325
3395
|
const normalizedAgent = agent.trim().toLowerCase();
|
|
3396
|
+
const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
|
|
3397
|
+
const id = existing?.id || crypto.randomUUID().slice(0, 8);
|
|
3326
3398
|
db2.prepare(`
|
|
3327
|
-
INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
|
|
3328
|
-
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
3399
|
+
INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
|
|
3400
|
+
VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
3329
3401
|
ON CONFLICT(agent) DO UPDATE SET
|
|
3330
3402
|
status = excluded.status,
|
|
3331
3403
|
last_seen_at = excluded.last_seen_at,
|
|
3404
|
+
session_id = COALESCE(excluded.session_id, agent_presence.session_id),
|
|
3332
3405
|
metadata = excluded.metadata
|
|
3333
|
-
`).run(normalizedAgent, resolvedStatus, metadataJson);
|
|
3406
|
+
`).run(id, normalizedAgent, sessionId ?? null, resolvedStatus, metadataJson);
|
|
3334
3407
|
}
|
|
3335
3408
|
function getPresence(agent) {
|
|
3336
3409
|
const db2 = getDb();
|
|
@@ -3341,12 +3414,11 @@ function getPresence(agent) {
|
|
|
3341
3414
|
function listAgents(opts) {
|
|
3342
3415
|
const db2 = getDb();
|
|
3343
3416
|
let query = "SELECT * FROM agent_presence";
|
|
3344
|
-
const params = [];
|
|
3345
3417
|
if (opts?.online_only) {
|
|
3346
3418
|
query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
|
|
3347
3419
|
}
|
|
3348
3420
|
query += " ORDER BY last_seen_at DESC";
|
|
3349
|
-
const rows = db2.prepare(query).all(
|
|
3421
|
+
const rows = db2.prepare(query).all();
|
|
3350
3422
|
return rows.map(parsePresence);
|
|
3351
3423
|
}
|
|
3352
3424
|
function removePresence(agent) {
|
|
@@ -3368,9 +3440,10 @@ function renameAgent(oldName, newName) {
|
|
|
3368
3440
|
db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
|
|
3369
3441
|
return true;
|
|
3370
3442
|
}
|
|
3371
|
-
var ONLINE_THRESHOLD_SECONDS = 60;
|
|
3443
|
+
var ONLINE_THRESHOLD_SECONDS = 60, CONFLICT_THRESHOLD_SECONDS;
|
|
3372
3444
|
var init_presence = __esm(() => {
|
|
3373
3445
|
init_db();
|
|
3446
|
+
CONFLICT_THRESHOLD_SECONDS = 30 * 60;
|
|
3374
3447
|
});
|
|
3375
3448
|
|
|
3376
3449
|
// src/lib/terminal-markdown.ts
|
|
@@ -3517,7 +3590,7 @@ var init_poll = __esm(() => {
|
|
|
3517
3590
|
var require_package = __commonJS((exports, module) => {
|
|
3518
3591
|
module.exports = {
|
|
3519
3592
|
name: "@hasna/conversations",
|
|
3520
|
-
version: "0.1.
|
|
3593
|
+
version: "0.1.27",
|
|
3521
3594
|
description: "Real-time CLI messaging for AI agents",
|
|
3522
3595
|
type: "module",
|
|
3523
3596
|
bin: {
|
|
@@ -30885,7 +30958,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
30885
30958
|
}
|
|
30886
30959
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
30887
30960
|
function getTime(strictTimeZone) {
|
|
30888
|
-
return function
|
|
30961
|
+
return function time(str) {
|
|
30889
30962
|
const matches = TIME.exec(str);
|
|
30890
30963
|
if (!matches)
|
|
30891
30964
|
return false;
|
|
@@ -31149,62 +31222,6 @@ class ExperimentalServerTasks {
|
|
|
31149
31222
|
requestStream(request, resultSchema, options) {
|
|
31150
31223
|
return this._server.requestStream(request, resultSchema, options);
|
|
31151
31224
|
}
|
|
31152
|
-
createMessageStream(params, options) {
|
|
31153
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
31154
|
-
if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
|
|
31155
|
-
throw new Error("Client does not support sampling tools capability.");
|
|
31156
|
-
}
|
|
31157
|
-
if (params.messages.length > 0) {
|
|
31158
|
-
const lastMessage = params.messages[params.messages.length - 1];
|
|
31159
|
-
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
|
31160
|
-
const hasToolResults = lastContent.some((c) => c.type === "tool_result");
|
|
31161
|
-
const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
|
|
31162
|
-
const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
|
|
31163
|
-
const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
|
|
31164
|
-
if (hasToolResults) {
|
|
31165
|
-
if (lastContent.some((c) => c.type !== "tool_result")) {
|
|
31166
|
-
throw new Error("The last message must contain only tool_result content if any is present");
|
|
31167
|
-
}
|
|
31168
|
-
if (!hasPreviousToolUse) {
|
|
31169
|
-
throw new Error("tool_result blocks are not matching any tool_use from the previous message");
|
|
31170
|
-
}
|
|
31171
|
-
}
|
|
31172
|
-
if (hasPreviousToolUse) {
|
|
31173
|
-
const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
|
|
31174
|
-
const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
|
|
31175
|
-
if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
|
|
31176
|
-
throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
|
|
31177
|
-
}
|
|
31178
|
-
}
|
|
31179
|
-
}
|
|
31180
|
-
return this.requestStream({
|
|
31181
|
-
method: "sampling/createMessage",
|
|
31182
|
-
params
|
|
31183
|
-
}, CreateMessageResultSchema, options);
|
|
31184
|
-
}
|
|
31185
|
-
elicitInputStream(params, options) {
|
|
31186
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
31187
|
-
const mode = params.mode ?? "form";
|
|
31188
|
-
switch (mode) {
|
|
31189
|
-
case "url": {
|
|
31190
|
-
if (!clientCapabilities?.elicitation?.url) {
|
|
31191
|
-
throw new Error("Client does not support url elicitation.");
|
|
31192
|
-
}
|
|
31193
|
-
break;
|
|
31194
|
-
}
|
|
31195
|
-
case "form": {
|
|
31196
|
-
if (!clientCapabilities?.elicitation?.form) {
|
|
31197
|
-
throw new Error("Client does not support form elicitation.");
|
|
31198
|
-
}
|
|
31199
|
-
break;
|
|
31200
|
-
}
|
|
31201
|
-
}
|
|
31202
|
-
const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
|
|
31203
|
-
return this.requestStream({
|
|
31204
|
-
method: "elicitation/create",
|
|
31205
|
-
params: normalizedParams
|
|
31206
|
-
}, ElicitResultSchema, options);
|
|
31207
|
-
}
|
|
31208
31225
|
async getTask(taskId, options) {
|
|
31209
31226
|
return this._server.getTask({ taskId }, options);
|
|
31210
31227
|
}
|
|
@@ -31218,9 +31235,6 @@ class ExperimentalServerTasks {
|
|
|
31218
31235
|
return this._server.cancelTask({ taskId }, options);
|
|
31219
31236
|
}
|
|
31220
31237
|
}
|
|
31221
|
-
var init_server = __esm(() => {
|
|
31222
|
-
init_types2();
|
|
31223
|
-
});
|
|
31224
31238
|
|
|
31225
31239
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
|
|
31226
31240
|
function assertToolsCallTaskCapability(requests, method, entityName) {
|
|
@@ -31259,12 +31273,11 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
|
|
|
31259
31273
|
|
|
31260
31274
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
|
|
31261
31275
|
var Server;
|
|
31262
|
-
var
|
|
31276
|
+
var init_server = __esm(() => {
|
|
31263
31277
|
init_protocol();
|
|
31264
31278
|
init_types2();
|
|
31265
31279
|
init_ajv_provider();
|
|
31266
31280
|
init_zod_compat();
|
|
31267
|
-
init_server();
|
|
31268
31281
|
Server = class Server extends Protocol {
|
|
31269
31282
|
constructor(_serverInfo, options) {
|
|
31270
31283
|
super(options);
|
|
@@ -32406,7 +32419,7 @@ function createCompletionResult(suggestions) {
|
|
|
32406
32419
|
}
|
|
32407
32420
|
var EMPTY_OBJECT_JSON_SCHEMA, EMPTY_COMPLETION_RESULT;
|
|
32408
32421
|
var init_mcp = __esm(() => {
|
|
32409
|
-
|
|
32422
|
+
init_server();
|
|
32410
32423
|
init_zod_compat();
|
|
32411
32424
|
init_zod_json_schema_compat();
|
|
32412
32425
|
init_types2();
|
|
@@ -33174,6 +33187,20 @@ var init_mcp2 = __esm(() => {
|
|
|
33174
33187
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
33175
33188
|
};
|
|
33176
33189
|
});
|
|
33190
|
+
server.registerTool("register_agent", {
|
|
33191
|
+
description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
|
|
33192
|
+
inputSchema: {
|
|
33193
|
+
name: exports_external.string(),
|
|
33194
|
+
session_id: exports_external.string(),
|
|
33195
|
+
role: exports_external.string().optional()
|
|
33196
|
+
}
|
|
33197
|
+
}, async (args) => {
|
|
33198
|
+
const { name, session_id, role } = args;
|
|
33199
|
+
const result = registerAgent(name, session_id, role);
|
|
33200
|
+
return {
|
|
33201
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
33202
|
+
};
|
|
33203
|
+
});
|
|
33177
33204
|
server.registerTool("heartbeat", {
|
|
33178
33205
|
description: "Send presence heartbeat.",
|
|
33179
33206
|
inputSchema: {
|
|
@@ -33305,6 +33332,7 @@ var init_mcp2 = __esm(() => {
|
|
|
33305
33332
|
"pin_message",
|
|
33306
33333
|
"unpin_message",
|
|
33307
33334
|
"get_pinned_messages",
|
|
33335
|
+
"register_agent",
|
|
33308
33336
|
"heartbeat",
|
|
33309
33337
|
"list_agents",
|
|
33310
33338
|
"get_blockers",
|
|
@@ -33350,6 +33378,7 @@ var init_mcp2 = __esm(() => {
|
|
|
33350
33378
|
pin_message: "Pin a message. Required: id",
|
|
33351
33379
|
unpin_message: "Unpin a message. Required: id",
|
|
33352
33380
|
get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
|
|
33381
|
+
register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
|
|
33353
33382
|
heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
|
|
33354
33383
|
list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
|
|
33355
33384
|
get_blockers: "Get unread blocking messages for agent. Optional: from?",
|
package/bin/mcp.js
CHANGED
|
@@ -6302,7 +6302,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
6302
6302
|
}
|
|
6303
6303
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6304
6304
|
function getTime(strictTimeZone) {
|
|
6305
|
-
return function
|
|
6305
|
+
return function time(str) {
|
|
6306
6306
|
const matches = TIME.exec(str);
|
|
6307
6307
|
if (!matches)
|
|
6308
6308
|
return false;
|
|
@@ -6600,9 +6600,13 @@ function getDb() {
|
|
|
6600
6600
|
`);
|
|
6601
6601
|
db.exec(`
|
|
6602
6602
|
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
6603
|
+
id TEXT NOT NULL,
|
|
6603
6604
|
agent TEXT PRIMARY KEY,
|
|
6605
|
+
session_id TEXT,
|
|
6606
|
+
role TEXT NOT NULL DEFAULT 'agent',
|
|
6604
6607
|
status TEXT NOT NULL DEFAULT 'online',
|
|
6605
6608
|
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
6609
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
6606
6610
|
metadata TEXT
|
|
6607
6611
|
)
|
|
6608
6612
|
`);
|
|
@@ -6681,6 +6685,25 @@ function getDb() {
|
|
|
6681
6685
|
db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
|
|
6682
6686
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
|
|
6683
6687
|
}
|
|
6688
|
+
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
6689
|
+
const presenceColNames = presenceCols.map((c) => c.name);
|
|
6690
|
+
if (!presenceColNames.includes("id")) {
|
|
6691
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
|
|
6692
|
+
const rows = db.prepare("SELECT agent FROM agent_presence").all();
|
|
6693
|
+
for (const row of rows) {
|
|
6694
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
6695
|
+
db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
if (!presenceColNames.includes("session_id")) {
|
|
6699
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
|
|
6700
|
+
}
|
|
6701
|
+
if (!presenceColNames.includes("role")) {
|
|
6702
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
|
|
6703
|
+
}
|
|
6704
|
+
if (!presenceColNames.includes("created_at")) {
|
|
6705
|
+
db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
|
|
6706
|
+
}
|
|
6684
6707
|
const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
6685
6708
|
if (!ftsExists) {
|
|
6686
6709
|
db.exec(`
|
|
@@ -27263,62 +27286,6 @@ class ExperimentalServerTasks {
|
|
|
27263
27286
|
requestStream(request, resultSchema, options) {
|
|
27264
27287
|
return this._server.requestStream(request, resultSchema, options);
|
|
27265
27288
|
}
|
|
27266
|
-
createMessageStream(params, options) {
|
|
27267
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
27268
|
-
if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
|
|
27269
|
-
throw new Error("Client does not support sampling tools capability.");
|
|
27270
|
-
}
|
|
27271
|
-
if (params.messages.length > 0) {
|
|
27272
|
-
const lastMessage = params.messages[params.messages.length - 1];
|
|
27273
|
-
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
|
27274
|
-
const hasToolResults = lastContent.some((c) => c.type === "tool_result");
|
|
27275
|
-
const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
|
|
27276
|
-
const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
|
|
27277
|
-
const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
|
|
27278
|
-
if (hasToolResults) {
|
|
27279
|
-
if (lastContent.some((c) => c.type !== "tool_result")) {
|
|
27280
|
-
throw new Error("The last message must contain only tool_result content if any is present");
|
|
27281
|
-
}
|
|
27282
|
-
if (!hasPreviousToolUse) {
|
|
27283
|
-
throw new Error("tool_result blocks are not matching any tool_use from the previous message");
|
|
27284
|
-
}
|
|
27285
|
-
}
|
|
27286
|
-
if (hasPreviousToolUse) {
|
|
27287
|
-
const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
|
|
27288
|
-
const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
|
|
27289
|
-
if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
|
|
27290
|
-
throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
|
|
27291
|
-
}
|
|
27292
|
-
}
|
|
27293
|
-
}
|
|
27294
|
-
return this.requestStream({
|
|
27295
|
-
method: "sampling/createMessage",
|
|
27296
|
-
params
|
|
27297
|
-
}, CreateMessageResultSchema, options);
|
|
27298
|
-
}
|
|
27299
|
-
elicitInputStream(params, options) {
|
|
27300
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
27301
|
-
const mode = params.mode ?? "form";
|
|
27302
|
-
switch (mode) {
|
|
27303
|
-
case "url": {
|
|
27304
|
-
if (!clientCapabilities?.elicitation?.url) {
|
|
27305
|
-
throw new Error("Client does not support url elicitation.");
|
|
27306
|
-
}
|
|
27307
|
-
break;
|
|
27308
|
-
}
|
|
27309
|
-
case "form": {
|
|
27310
|
-
if (!clientCapabilities?.elicitation?.form) {
|
|
27311
|
-
throw new Error("Client does not support form elicitation.");
|
|
27312
|
-
}
|
|
27313
|
-
break;
|
|
27314
|
-
}
|
|
27315
|
-
}
|
|
27316
|
-
const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
|
|
27317
|
-
return this.requestStream({
|
|
27318
|
-
method: "elicitation/create",
|
|
27319
|
-
params: normalizedParams
|
|
27320
|
-
}, ElicitResultSchema, options);
|
|
27321
|
-
}
|
|
27322
27289
|
async getTask(taskId, options) {
|
|
27323
27290
|
return this._server.getTask({ taskId }, options);
|
|
27324
27291
|
}
|
|
@@ -29778,6 +29745,7 @@ function updateCachedAutoName(newName) {
|
|
|
29778
29745
|
// src/lib/presence.ts
|
|
29779
29746
|
init_db();
|
|
29780
29747
|
var ONLINE_THRESHOLD_SECONDS = 60;
|
|
29748
|
+
var CONFLICT_THRESHOLD_SECONDS = 30 * 60;
|
|
29781
29749
|
function parsePresence(row) {
|
|
29782
29750
|
let metadata = null;
|
|
29783
29751
|
if (row.metadata) {
|
|
@@ -29792,36 +29760,85 @@ function parsePresence(row) {
|
|
|
29792
29760
|
const nowMs = Date.now();
|
|
29793
29761
|
const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
|
|
29794
29762
|
return {
|
|
29763
|
+
id: row.id || "",
|
|
29795
29764
|
agent: row.agent,
|
|
29765
|
+
session_id: row.session_id ?? null,
|
|
29766
|
+
role: row.role || "agent",
|
|
29796
29767
|
status: row.status,
|
|
29797
29768
|
last_seen_at: lastSeenAt,
|
|
29769
|
+
created_at: row.created_at || lastSeenAt,
|
|
29798
29770
|
online,
|
|
29799
29771
|
metadata
|
|
29800
29772
|
};
|
|
29801
29773
|
}
|
|
29802
|
-
function
|
|
29774
|
+
function isActiveSession(lastSeenAt) {
|
|
29775
|
+
const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
|
|
29776
|
+
const nowMs = Date.now();
|
|
29777
|
+
return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
|
|
29778
|
+
}
|
|
29779
|
+
function registerAgent(name, sessionId, role) {
|
|
29780
|
+
const db2 = getDb();
|
|
29781
|
+
const normalizedName = name.trim().toLowerCase();
|
|
29782
|
+
const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
29783
|
+
if (existing) {
|
|
29784
|
+
const lastSeenAt = existing.last_seen_at;
|
|
29785
|
+
const existingSessionId = existing.session_id;
|
|
29786
|
+
if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
|
|
29787
|
+
return {
|
|
29788
|
+
conflict: true,
|
|
29789
|
+
error: "agent_conflict",
|
|
29790
|
+
message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
|
|
29791
|
+
existing_id: existing.id,
|
|
29792
|
+
existing_name: normalizedName,
|
|
29793
|
+
existing_session_id: existingSessionId,
|
|
29794
|
+
last_seen_at: lastSeenAt,
|
|
29795
|
+
session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
|
|
29796
|
+
working_dir: null
|
|
29797
|
+
};
|
|
29798
|
+
}
|
|
29799
|
+
const tookOver = existingSessionId !== sessionId;
|
|
29800
|
+
db2.prepare(`
|
|
29801
|
+
UPDATE agent_presence
|
|
29802
|
+
SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
29803
|
+
WHERE agent = ?
|
|
29804
|
+
`).run(sessionId, role || existing.role || "agent", normalizedName);
|
|
29805
|
+
const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
29806
|
+
return { agent: parsePresence(updated), created: false, took_over: tookOver };
|
|
29807
|
+
}
|
|
29808
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
29809
|
+
const resolvedRole = role || "agent";
|
|
29810
|
+
db2.prepare(`
|
|
29811
|
+
INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
|
|
29812
|
+
VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
29813
|
+
`).run(id, normalizedName, sessionId, resolvedRole);
|
|
29814
|
+
const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
|
|
29815
|
+
return { agent: parsePresence(created), created: true, took_over: false };
|
|
29816
|
+
}
|
|
29817
|
+
function heartbeat(agent, status, metadata, sessionId) {
|
|
29803
29818
|
const db2 = getDb();
|
|
29804
29819
|
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
29805
29820
|
const resolvedStatus = status || "online";
|
|
29806
29821
|
const normalizedAgent = agent.trim().toLowerCase();
|
|
29822
|
+
const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
|
|
29823
|
+
const id = existing?.id || crypto.randomUUID().slice(0, 8);
|
|
29807
29824
|
db2.prepare(`
|
|
29808
|
-
INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
|
|
29809
|
-
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
29825
|
+
INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
|
|
29826
|
+
VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
29810
29827
|
ON CONFLICT(agent) DO UPDATE SET
|
|
29811
29828
|
status = excluded.status,
|
|
29812
29829
|
last_seen_at = excluded.last_seen_at,
|
|
29830
|
+
session_id = COALESCE(excluded.session_id, agent_presence.session_id),
|
|
29813
29831
|
metadata = excluded.metadata
|
|
29814
|
-
`).run(normalizedAgent, resolvedStatus, metadataJson);
|
|
29832
|
+
`).run(id, normalizedAgent, sessionId ?? null, resolvedStatus, metadataJson);
|
|
29815
29833
|
}
|
|
29816
29834
|
function listAgents(opts) {
|
|
29817
29835
|
const db2 = getDb();
|
|
29818
29836
|
let query = "SELECT * FROM agent_presence";
|
|
29819
|
-
const params = [];
|
|
29820
29837
|
if (opts?.online_only) {
|
|
29821
29838
|
query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
|
|
29822
29839
|
}
|
|
29823
29840
|
query += " ORDER BY last_seen_at DESC";
|
|
29824
|
-
const rows = db2.prepare(query).all(
|
|
29841
|
+
const rows = db2.prepare(query).all();
|
|
29825
29842
|
return rows.map(parsePresence);
|
|
29826
29843
|
}
|
|
29827
29844
|
function removePresence(agent) {
|
|
@@ -29846,7 +29863,7 @@ function renameAgent(oldName, newName) {
|
|
|
29846
29863
|
// package.json
|
|
29847
29864
|
var package_default = {
|
|
29848
29865
|
name: "@hasna/conversations",
|
|
29849
|
-
version: "0.1.
|
|
29866
|
+
version: "0.1.27",
|
|
29850
29867
|
description: "Real-time CLI messaging for AI agents",
|
|
29851
29868
|
type: "module",
|
|
29852
29869
|
bin: {
|
|
@@ -30555,6 +30572,20 @@ server.registerTool("get_pinned_messages", {
|
|
|
30555
30572
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30556
30573
|
};
|
|
30557
30574
|
});
|
|
30575
|
+
server.registerTool("register_agent", {
|
|
30576
|
+
description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
|
|
30577
|
+
inputSchema: {
|
|
30578
|
+
name: exports_external.string(),
|
|
30579
|
+
session_id: exports_external.string(),
|
|
30580
|
+
role: exports_external.string().optional()
|
|
30581
|
+
}
|
|
30582
|
+
}, async (args) => {
|
|
30583
|
+
const { name, session_id, role } = args;
|
|
30584
|
+
const result = registerAgent(name, session_id, role);
|
|
30585
|
+
return {
|
|
30586
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
30587
|
+
};
|
|
30588
|
+
});
|
|
30558
30589
|
server.registerTool("heartbeat", {
|
|
30559
30590
|
description: "Send presence heartbeat.",
|
|
30560
30591
|
inputSchema: {
|
|
@@ -30686,6 +30717,7 @@ server.registerTool("search_tools", {
|
|
|
30686
30717
|
"pin_message",
|
|
30687
30718
|
"unpin_message",
|
|
30688
30719
|
"get_pinned_messages",
|
|
30720
|
+
"register_agent",
|
|
30689
30721
|
"heartbeat",
|
|
30690
30722
|
"list_agents",
|
|
30691
30723
|
"get_blockers",
|
|
@@ -30731,6 +30763,7 @@ server.registerTool("describe_tools", {
|
|
|
30731
30763
|
pin_message: "Pin a message. Required: id",
|
|
30732
30764
|
unpin_message: "Unpin a message. Required: id",
|
|
30733
30765
|
get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
|
|
30766
|
+
register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
|
|
30734
30767
|
heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
|
|
30735
30768
|
list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
|
|
30736
30769
|
get_blockers: "Get unread blocking messages for agent. Optional: from?",
|