assistme 0.2.7 → 0.2.9

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.
@@ -1,15 +1,8 @@
1
- import { createClient, SupabaseClient } from "@supabase/supabase-js";
2
- import { createHash } from "crypto";
3
- import { getConfig } from "../utils/config.js";
4
- import { log } from "../utils/logger.js";
5
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
2
  import { join } from "path";
7
3
  import { homedir } from "os";
8
-
9
- // ── Well-Known Agent Profile IDs ─────────────────────────────────────
10
-
11
- export const CLI_AGENT_ID = "00000000-0000-0000-0000-000000000001";
12
- export const DAYBOX_AGENT_ID = "00000000-0000-0000-0000-000000000002";
4
+ import { callMcpHandler } from "./api-client.js";
5
+ import { log } from "../utils/logger.js";
13
6
 
14
7
  // ── Auth Store (persists am_ token to disk) ──────────────────────────
15
8
 
@@ -38,46 +31,11 @@ function writeAuthStore(data: Record<string, string>) {
38
31
  writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
39
32
  }
40
33
 
41
- // ── Supabase Client (anon key only, no auth session) ────────────────
42
-
43
- let supabase: SupabaseClient | null = null;
44
-
45
- export function getSupabase(): SupabaseClient {
46
- if (!supabase) {
47
- const config = getConfig();
48
- if (!config.supabaseUrl || !config.supabaseAnonKey) {
49
- throw new Error(
50
- "Supabase not configured. Run `assistme config set supabaseUrl <url>` first."
51
- );
52
- }
53
- supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
54
- auth: { persistSession: false },
55
- });
56
- }
57
- return supabase;
58
- }
59
-
60
- // ── Token Hash ──────────────────────────────────────────────────────
61
-
62
- function hashToken(token: string): string {
63
- return createHash("sha256").update(token).digest("hex");
64
- }
65
-
66
- /** Get stored token hash (computed from persisted am_ token). */
67
- function getTokenHash(): string {
68
- const store = readAuthStore();
69
- const token = store["mcp_token"];
70
- if (!token || !token.startsWith("am_")) {
71
- throw new Error("Not authenticated. Run `assistme login`.");
72
- }
73
- return hashToken(token);
74
- }
75
-
76
34
  // ── Auth ─────────────────────────────────────────────────────────────
77
35
 
78
36
  /**
79
37
  * Login using an am_ MCP token.
80
- * Validates against DB via validate_mcp_token RPC, stores locally.
38
+ * Validates against DB via edge function, stores locally.
81
39
  */
82
40
  export async function loginWithToken(mcpToken: string): Promise<string> {
83
41
  if (!mcpToken.startsWith("am_")) {
@@ -86,34 +44,25 @@ export async function loginWithToken(mcpToken: string): Promise<string> {
86
44
  );
87
45
  }
88
46
 
89
- const hash = hashToken(mcpToken);
90
- const sb = getSupabase();
91
-
92
- const { data, error } = await sb.rpc("validate_mcp_token", {
93
- p_token_hash: hash,
94
- });
95
-
96
- if (error) throw new Error(`Token validation failed: ${error.message}`);
97
- if (!data || data.length === 0) throw new Error("Invalid or expired token");
47
+ const result = await callMcpHandler<{ user_id: string; email: string | null }>(
48
+ "auth.validate_token",
49
+ {},
50
+ mcpToken,
51
+ );
98
52
 
99
53
  // Persist token
100
54
  const store = readAuthStore();
101
55
  store["mcp_token"] = mcpToken;
102
56
  writeAuthStore(store);
103
57
 
104
- return data[0].out_user_id;
58
+ return result.user_id;
105
59
  }
106
60
 
107
61
  export async function getCurrentUserId(): Promise<string> {
108
- const hash = getTokenHash();
109
- const sb = getSupabase();
110
- const { data, error } = await sb.rpc("validate_mcp_token", {
111
- p_token_hash: hash,
112
- });
113
- if (error || !data || data.length === 0) {
114
- throw new Error("Token expired or revoked. Run `assistme login`.");
115
- }
116
- return data[0].out_user_id;
62
+ const result = await callMcpHandler<{ user_id: string }>(
63
+ "auth.validate_token",
64
+ );
65
+ return result.user_id;
117
66
  }
118
67
 
119
68
  export async function logout(): Promise<void> {
@@ -145,61 +94,71 @@ export async function createSession(
145
94
  workspacePath: string,
146
95
  version: string
147
96
  ): Promise<AgentSession> {
148
- const sb = getSupabase();
149
- const { data, error } = await sb.rpc("mcp_create_session", {
150
- p_token_hash: getTokenHash(),
151
- p_session_name: sessionName,
152
- p_workspace_path: workspacePath,
153
- p_version: version,
154
- p_model: getConfig().model || null,
97
+ const { getConfig } = await import("../utils/config.js");
98
+ const data = await callMcpHandler<AgentSession>("session.create", {
99
+ session_name: sessionName,
100
+ workspace_path: workspacePath,
101
+ version,
102
+ model: getConfig().model || null,
155
103
  });
156
-
157
- if (error) throw new Error(`Failed to create session: ${error.message}`);
158
- return data as AgentSession;
104
+ return data;
159
105
  }
160
106
 
161
107
  export async function updateHeartbeat(sessionId: string): Promise<void> {
162
- const sb = getSupabase();
163
- const { error } = await sb.rpc("mcp_heartbeat", {
164
- p_token_hash: getTokenHash(),
165
- p_session_id: sessionId,
166
- });
167
- if (error) log.warn(`Heartbeat update failed: ${error.message}`);
108
+ try {
109
+ await callMcpHandler("session.heartbeat", { session_id: sessionId });
110
+ } catch (err) {
111
+ log.warn(`Heartbeat update failed: ${err instanceof Error ? err.message : err}`);
112
+ }
168
113
  }
169
114
 
170
115
  export async function endSession(sessionId: string): Promise<void> {
171
- const sb = getSupabase();
172
- const { error } = await sb.rpc("mcp_end_session", {
173
- p_token_hash: getTokenHash(),
174
- p_session_id: sessionId,
175
- });
176
- if (error) log.error(`Failed to end session: ${error.message}`);
116
+ try {
117
+ await callMcpHandler("session.end", { session_id: sessionId });
118
+ } catch (err) {
119
+ log.error(`Failed to end session: ${err instanceof Error ? err.message : err}`);
120
+ }
177
121
  }
178
122
 
179
123
  export async function setSessionBusy(
180
124
  sessionId: string,
181
125
  busy: boolean
182
126
  ): Promise<void> {
183
- const sb = getSupabase();
184
- await sb.rpc("mcp_set_session_busy", {
185
- p_token_hash: getTokenHash(),
186
- p_session_id: sessionId,
187
- p_busy: busy,
127
+ await callMcpHandler("session.set_busy", {
128
+ session_id: sessionId,
129
+ busy,
188
130
  });
189
131
  }
190
132
 
133
+ export async function cleanupStaleSessions(
134
+ currentSessionId: string,
135
+ thresholdMs = 120_000
136
+ ): Promise<number> {
137
+ try {
138
+ const result = await callMcpHandler<{ cleaned: number }>(
139
+ "session.cleanup_stale",
140
+ { current_session_id: currentSessionId, threshold_ms: thresholdMs },
141
+ );
142
+ return result.cleaned;
143
+ } catch {
144
+ return 0;
145
+ }
146
+ }
147
+
148
+ export async function getActiveSessions(
149
+ limit = 5
150
+ ): Promise<AgentSession[]> {
151
+ return callMcpHandler<AgentSession[]>("session.get_active", { limit });
152
+ }
153
+
191
154
  // ── Conversation Management ─────────────────────────────────────────
192
155
 
193
156
  export async function getOrCreateCliConversation(
194
157
  _userId: string,
195
158
  _sessionId: string
196
159
  ): Promise<string> {
197
- const sb = getSupabase();
198
- const { data, error } = await sb.rpc("mcp_get_or_create_conversation", {
199
- p_token_hash: getTokenHash(),
200
- });
201
- if (error) throw new Error(`Failed to get conversation: ${error.message}`);
202
- return data as string;
160
+ const data = await callMcpHandler<string>("conversation.get_or_create");
161
+ return data;
203
162
  }
204
163
 
205
164
  // ── Message / Task Management ────────────────────────────────────────
@@ -228,77 +187,46 @@ export async function createTask(
228
187
  sessionId: string,
229
188
  prompt: string
230
189
  ): Promise<ConversationMessage> {
231
- const sb = getSupabase();
232
- const { data, error } = await sb.rpc("mcp_create_task", {
233
- p_token_hash: getTokenHash(),
234
- p_conversation_id: conversationId,
235
- p_session_id: sessionId,
236
- p_prompt: prompt,
190
+ const data = await callMcpHandler<Record<string, unknown>>("task.create", {
191
+ conversation_id: conversationId,
192
+ session_id: sessionId,
193
+ prompt,
237
194
  });
238
-
239
- if (error) throw new Error(`Failed to create task: ${error.message}`);
240
195
  return { ...data, prompt } as ConversationMessage;
241
196
  }
242
197
 
243
- export async function pollPendingTasks(
244
- sessionId: string
245
- ): Promise<ConversationMessage[]> {
246
- const sb = getSupabase();
247
- const { data, error } = await sb.rpc("mcp_poll_tasks", {
248
- p_token_hash: getTokenHash(),
249
- p_session_id: sessionId,
250
- });
251
-
252
- if (error) {
253
- log.warn(`Task poll failed: ${error.message}`);
254
- return [];
255
- }
256
-
257
- const rows = (data || []) as Record<string, unknown>[];
258
- return rows.map((row) => ({
259
- ...row,
260
- prompt:
261
- (row.metadata as Record<string, unknown>)?.prompt || row.content || "",
262
- })) as ConversationMessage[];
263
- }
264
-
265
198
  /**
266
- * Atomically poll ONE pending task and claim it in a single DB call.
199
+ * Atomically poll ONE pending task and claim it in a single call.
267
200
  * Uses FOR UPDATE SKIP LOCKED — concurrent CLIs will never grab the same task.
268
201
  * Returns null if no pending task exists.
269
202
  */
270
203
  export async function pollAndClaimTask(
271
204
  sessionId: string
272
205
  ): Promise<ConversationMessage | null> {
273
- const sb = getSupabase();
274
- const { data, error } = await sb.rpc("mcp_poll_and_claim_task", {
275
- p_token_hash: getTokenHash(),
276
- p_session_id: sessionId,
277
- });
206
+ try {
207
+ const data = await callMcpHandler<Record<string, unknown> | null>(
208
+ "task.poll_and_claim",
209
+ { session_id: sessionId },
210
+ );
211
+
212
+ if (!data) return null;
278
213
 
279
- if (error) {
280
- log.warn(`Poll-and-claim failed: ${error.message}`);
214
+ return {
215
+ ...data,
216
+ prompt:
217
+ (data.metadata as Record<string, unknown>)?.prompt || data.content || "",
218
+ } as ConversationMessage;
219
+ } catch (err) {
220
+ log.warn(`Poll-and-claim failed: ${err instanceof Error ? err.message : err}`);
281
221
  return null;
282
222
  }
283
-
284
- if (!data) return null;
285
-
286
- const row = data as Record<string, unknown>;
287
- return {
288
- ...row,
289
- prompt:
290
- (row.metadata as Record<string, unknown>)?.prompt || row.content || "",
291
- } as ConversationMessage;
292
223
  }
293
224
 
294
225
  export async function claimTask(messageId: string): Promise<boolean> {
295
- const sb = getSupabase();
296
- const { data, error } = await sb.rpc("mcp_claim_task", {
297
- p_token_hash: getTokenHash(),
298
- p_message_id: messageId,
226
+ const data = await callMcpHandler<boolean>("task.claim", {
227
+ message_id: messageId,
299
228
  });
300
- if (error) throw new Error(`Failed to claim task: ${error.message}`);
301
- return data as boolean;
229
+ return data;
302
230
  }
303
231
 
304
232
  export async function completeTask(
@@ -306,27 +234,25 @@ export async function completeTask(
306
234
  resultSummary: string,
307
235
  tokenUsage?: Record<string, number>
308
236
  ): Promise<void> {
309
- const sb = getSupabase();
310
- const { error } = await sb.rpc("mcp_complete_task", {
311
- p_token_hash: getTokenHash(),
312
- p_message_id: messageId,
313
- p_result: resultSummary,
314
- p_token_usage: tokenUsage || null,
237
+ await callMcpHandler("task.complete", {
238
+ message_id: messageId,
239
+ result: resultSummary,
240
+ token_usage: tokenUsage || null,
315
241
  });
316
- if (error) throw new Error(`Failed to complete task: ${error.message}`);
317
242
  }
318
243
 
319
244
  export async function failTask(
320
245
  messageId: string,
321
246
  errorMessage: string
322
247
  ): Promise<void> {
323
- const sb = getSupabase();
324
- const { error } = await sb.rpc("mcp_fail_task", {
325
- p_token_hash: getTokenHash(),
326
- p_message_id: messageId,
327
- p_error: errorMessage,
328
- });
329
- if (error) log.error(`Failed to update task status: ${error.message}`);
248
+ try {
249
+ await callMcpHandler("task.fail", {
250
+ message_id: messageId,
251
+ error: errorMessage,
252
+ });
253
+ } catch (err) {
254
+ log.error(`Failed to update task status: ${err instanceof Error ? err.message : err}`);
255
+ }
330
256
  }
331
257
 
332
258
  // ── Job Run Polling ─────────────────────────────────────────────────
@@ -344,20 +270,17 @@ export interface PendingJobRun {
344
270
  * Returns null if no pending job run exists.
345
271
  */
346
272
  export async function pollAndClaimJobRun(
347
- userId: string
273
+ _userId: string
348
274
  ): Promise<PendingJobRun | null> {
349
- const sb = getSupabase();
350
- const { data, error } = await sb.rpc("claim_pending_job_run", {
351
- p_user_id: userId,
352
- });
353
-
354
- if (error) {
355
- log.debug(`Job run poll failed: ${error.message}`);
275
+ try {
276
+ const data = await callMcpHandler<PendingJobRun | null>(
277
+ "job.claim_pending_run",
278
+ );
279
+ return data;
280
+ } catch (err) {
281
+ log.debug(`Job run poll failed: ${err instanceof Error ? err.message : err}`);
356
282
  return null;
357
283
  }
358
-
359
- if (!data) return null;
360
- return data as PendingJobRun;
361
284
  }
362
285
 
363
286
  // ── Conversation History ─────────────────────────────────────────────
@@ -376,37 +299,33 @@ export async function getConversationHistory(
376
299
  excludeMessageId: string,
377
300
  limit: number = 20
378
301
  ): Promise<HistoryEntry[]> {
379
- const sb = getSupabase();
380
-
381
- const { data, error } = await sb
382
- .from("conversation_messages")
383
- .select("id, role, content, status, metadata, created_at")
384
- .eq("conversation_id", conversationId)
385
- .in("status", ["completed", "failed"])
386
- .neq("id", excludeMessageId)
387
- .order("created_at", { ascending: false })
388
- .limit(limit);
389
-
390
- if (error) {
391
- log.debug(`Failed to fetch conversation history: ${error.message}`);
302
+ try {
303
+ const rows = await callMcpHandler<Array<Record<string, unknown>>>(
304
+ "conversation.get_history",
305
+ {
306
+ conversation_id: conversationId,
307
+ exclude_message_id: excludeMessageId,
308
+ limit,
309
+ },
310
+ );
311
+
312
+ return (rows || [])
313
+ .reverse() // chronological order (oldest first)
314
+ .map((row) => {
315
+ const prompt =
316
+ ((row.metadata as Record<string, unknown>)?.prompt as string) || "";
317
+ const content = (row.content as string) || "";
318
+ const response =
319
+ row.status === "failed"
320
+ ? `[Task failed] ${content}`
321
+ : content;
322
+ return { prompt, response };
323
+ })
324
+ .filter((entry) => entry.prompt && entry.response);
325
+ } catch (err) {
326
+ log.debug(`Failed to fetch conversation history: ${err instanceof Error ? err.message : err}`);
392
327
  return [];
393
328
  }
394
-
395
- const rows = (data || []) as Array<Record<string, unknown>>;
396
-
397
- return rows
398
- .reverse() // chronological order (oldest first)
399
- .map((row) => {
400
- const prompt =
401
- ((row.metadata as Record<string, unknown>)?.prompt as string) || "";
402
- const content = (row.content as string) || "";
403
- const response =
404
- row.status === "failed"
405
- ? `[Task failed] ${content}`
406
- : content;
407
- return { prompt, response };
408
- })
409
- .filter((entry) => entry.prompt && entry.response);
410
329
  }
411
330
 
412
331
  // ── Event Streaming ─────────────────────────────────────────────────
@@ -432,33 +351,17 @@ export async function emitEvent(
432
351
  eventType: EventType,
433
352
  eventData: Record<string, unknown>
434
353
  ): Promise<void> {
435
- const sb = getSupabase();
436
354
  eventSequence++;
437
- const { error } = await sb.rpc("mcp_emit_event", {
438
- p_token_hash: getTokenHash(),
439
- p_message_id: messageId,
440
- p_event_type: eventType,
441
- p_event_data: eventData,
442
- p_seq: eventSequence,
443
- });
444
- if (error) log.warn(`Failed to emit event: ${error.message}`);
445
- }
446
-
447
- export async function emitEvents(
448
- messageId: string,
449
- events: Array<{ type: EventType; data: Record<string, unknown> }>
450
- ): Promise<void> {
451
- const sb = getSupabase();
452
- const eventsJson = events.map((e) => {
453
- eventSequence++;
454
- return { type: e.type, data: e.data, seq: eventSequence };
455
- });
456
- const { error } = await sb.rpc("mcp_emit_events", {
457
- p_token_hash: getTokenHash(),
458
- p_message_id: messageId,
459
- p_events: eventsJson,
460
- });
461
- if (error) log.warn(`Failed to emit events batch: ${error.message}`);
355
+ try {
356
+ await callMcpHandler("event.emit", {
357
+ message_id: messageId,
358
+ event_type: eventType,
359
+ event_data: eventData,
360
+ seq: eventSequence,
361
+ });
362
+ } catch (err) {
363
+ log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
364
+ }
462
365
  }
463
366
 
464
367
  // ── Action Request Helpers ──────────────────────────────────────────
@@ -467,23 +370,17 @@ export async function setActionRequest(
467
370
  messageId: string,
468
371
  actionData: Record<string, unknown>
469
372
  ): Promise<void> {
470
- const sb = getSupabase();
471
- const { error } = await sb.rpc("mcp_set_action_request", {
472
- p_token_hash: getTokenHash(),
473
- p_message_id: messageId,
474
- p_action_data: actionData,
373
+ await callMcpHandler("action.set_request", {
374
+ message_id: messageId,
375
+ action_data: actionData,
475
376
  });
476
- if (error) throw new Error(`Failed to set action request: ${error.message}`);
477
377
  }
478
378
 
479
379
  export async function pollActionResponse(
480
380
  messageId: string
481
381
  ): Promise<Record<string, unknown> | null> {
482
- const sb = getSupabase();
483
- const { data, error } = await sb.rpc("mcp_poll_action_response", {
484
- p_token_hash: getTokenHash(),
485
- p_message_id: messageId,
486
- });
487
- if (error) throw new Error(`Failed to poll action response: ${error.message}`);
488
- return data as Record<string, unknown> | null;
382
+ return callMcpHandler<Record<string, unknown> | null>(
383
+ "action.poll_response",
384
+ { message_id: messageId },
385
+ );
489
386
  }