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,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
- import { getCurrentUserId, getSupabase } from "../db/supabase.js";
3
+ import { getCurrentUserId } from "../db/supabase.js";
4
+ import { callMcpHandler } from "../db/api-client.js";
4
5
  import { log } from "../utils/logger.js";
5
6
  import {
6
7
  createScheduledTask,
@@ -150,11 +151,10 @@ export function registerJobCommands(program: Command): void {
150
151
  );
151
152
 
152
153
  // Link job_id
153
- const sb = getSupabase();
154
- await sb
155
- .from("agent_scheduled_tasks")
156
- .update({ job_id: job.jobId })
157
- .eq("id", task.id);
154
+ await callMcpHandler("schedule.link_job", {
155
+ task_id: task.id,
156
+ job_id: job.jobId,
157
+ });
158
158
 
159
159
  log.success(`Job "${name}" scheduled: ${opts.cron} (${tz})`);
160
160
  console.log(` Skills: ${job.skills.length}`);
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
- import { getCurrentUserId, getSupabase } from "../db/supabase.js";
3
+ import { getCurrentUserId, getActiveSessions } from "../db/supabase.js";
4
4
  import { log } from "../utils/logger.js";
5
5
 
6
6
  export function registerStatusCommand(program: Command): void {
@@ -9,16 +9,9 @@ export function registerStatusCommand(program: Command): void {
9
9
  .description("Check the status of the current agent session")
10
10
  .action(async () => {
11
11
  try {
12
- const userId = await getCurrentUserId();
13
- const sb = getSupabase();
12
+ await getCurrentUserId();
14
13
 
15
- const { data: sessions } = await sb
16
- .from("agent_sessions")
17
- .select("*")
18
- .eq("user_id", userId)
19
- .in("status", ["online", "busy"])
20
- .order("started_at", { ascending: false })
21
- .limit(5);
14
+ const sessions = await getActiveSessions(5);
22
15
 
23
16
  if (!sessions || sessions.length === 0) {
24
17
  console.log(chalk.yellow("No active sessions found."));
@@ -0,0 +1,68 @@
1
+ import { getConfig } from "../utils/config.js";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ // ── Auth Store (shared with supabase.ts) ────────────────────────────
7
+
8
+ const AUTH_DIR = join(homedir(), ".config", "assistme");
9
+ const AUTH_FILE = join(AUTH_DIR, "auth.json");
10
+
11
+ function readAuthStore(): Record<string, string> {
12
+ try {
13
+ if (existsSync(AUTH_FILE)) {
14
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
15
+ }
16
+ } catch {
17
+ // Corrupted file — start fresh
18
+ }
19
+ return {};
20
+ }
21
+
22
+ /**
23
+ * Get the raw MCP token from the local auth store.
24
+ * Throws if not authenticated.
25
+ */
26
+ export function getRawToken(): string {
27
+ const store = readAuthStore();
28
+ const token = store["mcp_token"];
29
+ if (!token || !token.startsWith("am_")) {
30
+ throw new Error("Not authenticated. Run `assistme login`.");
31
+ }
32
+ return token;
33
+ }
34
+
35
+ /**
36
+ * Call the mcp-handler edge function.
37
+ *
38
+ * @param action Dot-separated action (e.g. "session.create", "task.poll_and_claim")
39
+ * @param params Action parameters
40
+ * @param overrideToken Optional token override (used during login before token is persisted)
41
+ */
42
+ export async function callMcpHandler<T = unknown>(
43
+ action: string,
44
+ params: Record<string, unknown> = {},
45
+ overrideToken?: string,
46
+ ): Promise<T> {
47
+ const config = getConfig();
48
+ const token = overrideToken || getRawToken();
49
+ const url = `${config.supabaseUrl}/functions/v1/mcp-handler`;
50
+
51
+ const response = await fetch(url, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ Authorization: `Bearer ${token}`,
56
+ apikey: config.supabaseAnonKey,
57
+ },
58
+ body: JSON.stringify({ action, params }),
59
+ });
60
+
61
+ const body = await response.json();
62
+
63
+ if (!response.ok || body.error) {
64
+ throw new Error(body.error || `Request failed: ${response.status}`);
65
+ }
66
+
67
+ return body.data as T;
68
+ }
@@ -1,81 +1,11 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
- // ── Supabase Mock ────────────────────────────────────────────────
4
- // The supabase module caches the client singleton. We use a STABLE mock
5
- // that persists across tests, with `_setResult()` to reconfigure per-test.
3
+ // ── callMcpHandler Mock ─────────────────────────────────────────
4
+ const mockCallMcpHandler = vi.fn();
6
5
 
7
- let finalResult: Record<string, unknown> = { data: [], error: null };
8
-
9
- const chain: Record<string, unknown> = {};
10
- const chainMethods = [
11
- "select",
12
- "insert",
13
- "update",
14
- "delete",
15
- "eq",
16
- "neq",
17
- "not",
18
- "or",
19
- "in",
20
- "order",
21
- "limit",
22
- "single",
23
- "from",
24
- "lt",
25
- ];
26
- for (const method of chainMethods) {
27
- chain[method] = vi.fn().mockImplementation(() => chain);
28
- }
29
- // Make chain thenable — always resolves to current finalResult
30
- chain.then = function (resolve: (value: unknown) => void) {
31
- return resolve(finalResult);
32
- };
33
- chain.single = vi.fn().mockImplementation(() => ({
34
- ...chain,
35
- then: (resolve: (value: unknown) => void) => resolve(finalResult),
36
- }));
37
-
38
- const mockFrom = vi.fn().mockReturnValue(chain);
39
- const mockRpc = vi.fn().mockImplementation(() => ({
40
- then: (resolve: (value: unknown) => void) => resolve(finalResult),
41
- }));
42
- const mockAuth = {
43
- setSession: vi.fn().mockResolvedValue({
44
- data: { user: { id: "user-123" } },
45
- error: null,
46
- }),
47
- getSession: vi.fn().mockResolvedValue({
48
- data: { session: { access_token: "test" } },
49
- }),
50
- getUser: vi.fn().mockResolvedValue({
51
- data: { user: { id: "user-123" } },
52
- error: null,
53
- }),
54
- signOut: vi.fn().mockResolvedValue(undefined),
55
- };
56
-
57
- const stableMockClient = {
58
- from: mockFrom,
59
- rpc: mockRpc,
60
- auth: mockAuth,
61
- };
62
-
63
- function setResult(result: Record<string, unknown>) {
64
- finalResult = result;
65
- chain.then = function (resolve: (value: unknown) => void) {
66
- return resolve(result);
67
- };
68
- chain.single = vi.fn().mockImplementation(() => ({
69
- ...chain,
70
- then: (resolve: (value: unknown) => void) => resolve(result),
71
- }));
72
- mockRpc.mockImplementation(() => ({
73
- then: (resolve: (value: unknown) => void) => resolve(result),
74
- }));
75
- }
76
-
77
- vi.mock("@supabase/supabase-js", () => ({
78
- createClient: () => stableMockClient,
6
+ vi.mock("./api-client.js", () => ({
7
+ callMcpHandler: (...args: unknown[]) => mockCallMcpHandler(...args),
8
+ getRawToken: () => "am_test_token_123",
79
9
  }));
80
10
 
81
11
  vi.mock("../utils/config.js", () => ({
@@ -112,7 +42,6 @@ const {
112
42
  updateHeartbeat,
113
43
  endSession,
114
44
  setSessionBusy,
115
- pollPendingTasks,
116
45
  claimTask,
117
46
  completeTask,
118
47
  failTask,
@@ -122,48 +51,38 @@ const {
122
51
  getCurrentUserId,
123
52
  } = await import("./supabase.js");
124
53
 
125
- describe("Supabase DB Layer", () => {
54
+ describe("Supabase DB Layer (edge function)", () => {
126
55
  beforeEach(() => {
127
56
  vi.clearAllMocks();
128
- // Restore chain mocks (clearAllMocks resets them)
129
- for (const method of chainMethods) {
130
- chain[method] = vi.fn().mockImplementation(() => chain);
131
- }
132
- chain.single = vi.fn().mockImplementation(() => ({
133
- ...chain,
134
- then: (resolve: (value: unknown) => void) => resolve(finalResult),
135
- }));
136
- mockFrom.mockReturnValue(chain);
137
- setResult({ data: [], error: null });
138
57
  resetEventSequence();
139
58
  });
140
59
 
141
60
  describe("createSession()", () => {
142
- it("inserts a new session and returns it", async () => {
61
+ it("calls session.create action and returns session data", async () => {
143
62
  const sessionData = {
144
63
  id: "sess-001",
145
64
  user_id: "user-123",
146
65
  session_name: "Test",
147
66
  status: "online",
148
67
  };
149
- setResult({ data: sessionData, error: null });
68
+ mockCallMcpHandler.mockResolvedValueOnce(sessionData);
150
69
 
151
70
  const result = await createSession("user-123", "Test", "/tmp", "0.1.0");
152
71
 
153
- expect(mockRpc).toHaveBeenCalledWith(
154
- "mcp_create_session",
72
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
73
+ "session.create",
155
74
  expect.objectContaining({
156
- p_session_name: "Test",
157
- p_workspace_path: "/tmp",
158
- p_version: "0.1.0",
75
+ session_name: "Test",
76
+ workspace_path: "/tmp",
77
+ version: "0.1.0",
159
78
  })
160
79
  );
161
80
  expect(result.id).toBe("sess-001");
162
81
  expect(result.status).toBe("online");
163
82
  });
164
83
 
165
- it("throws on DB error", async () => {
166
- setResult({ data: null, error: { message: "insert failed" } });
84
+ it("throws on edge function error", async () => {
85
+ mockCallMcpHandler.mockRejectedValueOnce(new Error("Failed to create session"));
167
86
 
168
87
  await expect(createSession("user-123", "Test", "/tmp", "0.1.0")).rejects.toThrow(
169
88
  "Failed to create session"
@@ -172,152 +91,120 @@ describe("Supabase DB Layer", () => {
172
91
  });
173
92
 
174
93
  describe("updateHeartbeat()", () => {
175
- it("updates the heartbeat timestamp", async () => {
176
- setResult({ error: null });
94
+ it("calls session.heartbeat action", async () => {
95
+ mockCallMcpHandler.mockResolvedValueOnce(undefined);
177
96
  await updateHeartbeat("sess-001");
178
- expect(mockRpc).toHaveBeenCalledWith(
179
- "mcp_heartbeat",
97
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
98
+ "session.heartbeat",
180
99
  expect.objectContaining({
181
- p_session_id: "sess-001",
100
+ session_id: "sess-001",
182
101
  })
183
102
  );
184
103
  });
185
104
  });
186
105
 
187
106
  describe("endSession()", () => {
188
- it("sets session to offline", async () => {
189
- setResult({ error: null });
107
+ it("calls session.end action", async () => {
108
+ mockCallMcpHandler.mockResolvedValueOnce(undefined);
190
109
  await endSession("sess-001");
191
- expect(mockRpc).toHaveBeenCalledWith(
192
- "mcp_end_session",
110
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
111
+ "session.end",
193
112
  expect.objectContaining({
194
- p_session_id: "sess-001",
113
+ session_id: "sess-001",
195
114
  })
196
115
  );
197
116
  });
198
117
  });
199
118
 
200
119
  describe("setSessionBusy()", () => {
201
- it("sets session to busy", async () => {
202
- setResult({ error: null });
120
+ it("calls session.set_busy action", async () => {
121
+ mockCallMcpHandler.mockResolvedValueOnce(undefined);
203
122
  await setSessionBusy("sess-001", true);
204
- expect(mockRpc).toHaveBeenCalledWith(
205
- "mcp_set_session_busy",
123
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
124
+ "session.set_busy",
206
125
  expect.objectContaining({
207
- p_session_id: "sess-001",
208
- p_busy: true,
126
+ session_id: "sess-001",
127
+ busy: true,
209
128
  })
210
129
  );
211
130
  });
212
131
  });
213
132
 
214
- describe("pollPendingTasks()", () => {
215
- it("returns empty array when no tasks", async () => {
216
- setResult({ data: [], error: null });
217
- const tasks = await pollPendingTasks("sess-001");
218
- expect(tasks).toEqual([]);
219
- });
220
-
221
- it("returns tasks with prompt extracted from metadata", async () => {
222
- setResult({
223
- data: [
224
- {
225
- id: "msg-001",
226
- session_id: "sess-001",
227
- status: "pending",
228
- content: "",
229
- metadata: { prompt: "hello world" },
230
- },
231
- ],
232
- error: null,
233
- });
234
-
235
- const tasks = await pollPendingTasks("sess-001");
236
- expect(tasks).toHaveLength(1);
237
- expect(tasks[0].prompt).toBe("hello world");
238
- });
239
-
240
- it("returns empty array on error", async () => {
241
- setResult({ data: null, error: { message: "query failed" } });
242
- const tasks = await pollPendingTasks("sess-001");
243
- expect(tasks).toEqual([]);
244
- });
245
- });
246
-
247
133
  describe("claimTask()", () => {
248
- it("updates status to running", async () => {
249
- setResult({ error: null });
134
+ it("calls task.claim action", async () => {
135
+ mockCallMcpHandler.mockResolvedValueOnce(true);
250
136
  await claimTask("msg-001");
251
- expect(mockRpc).toHaveBeenCalledWith(
252
- "mcp_claim_task",
137
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
138
+ "task.claim",
253
139
  expect.objectContaining({
254
- p_message_id: "msg-001",
140
+ message_id: "msg-001",
255
141
  })
256
142
  );
257
143
  });
258
-
259
- it("throws on DB error", async () => {
260
- setResult({ error: { message: "claim failed" } });
261
- await expect(claimTask("msg-001")).rejects.toThrow("Failed to claim task");
262
- });
263
144
  });
264
145
 
265
146
  describe("completeTask()", () => {
266
- it("updates status to completed with result", async () => {
267
- setResult({ data: { metadata: {} }, error: null });
147
+ it("calls task.complete action", async () => {
148
+ mockCallMcpHandler.mockResolvedValueOnce(undefined);
268
149
  await completeTask("msg-001", "Task completed successfully");
269
- expect(mockRpc).toHaveBeenCalledWith(
270
- "mcp_complete_task",
150
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
151
+ "task.complete",
271
152
  expect.objectContaining({
272
- p_message_id: "msg-001",
273
- p_result: "Task completed successfully",
153
+ message_id: "msg-001",
154
+ result: "Task completed successfully",
274
155
  })
275
156
  );
276
157
  });
277
158
  });
278
159
 
279
160
  describe("failTask()", () => {
280
- it("updates status to failed with error", async () => {
281
- setResult({ data: { metadata: {} }, error: null });
161
+ it("calls task.fail action", async () => {
162
+ mockCallMcpHandler.mockResolvedValueOnce(undefined);
282
163
  await failTask("msg-001", "Something went wrong");
283
- expect(mockRpc).toHaveBeenCalledWith(
284
- "mcp_fail_task",
164
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
165
+ "task.fail",
285
166
  expect.objectContaining({
286
- p_message_id: "msg-001",
287
- p_error: "Something went wrong",
167
+ message_id: "msg-001",
168
+ error: "Something went wrong",
288
169
  })
289
170
  );
290
171
  });
291
172
  });
292
173
 
293
174
  describe("emitEvent()", () => {
294
- it("inserts events to message_events table", async () => {
295
- setResult({ error: null });
175
+ it("calls event.emit action with sequence number", async () => {
176
+ mockCallMcpHandler.mockResolvedValue(undefined);
296
177
 
297
178
  await emitEvent("msg-001", "text_delta", { text: "hello" });
298
179
  await emitEvent("msg-001", "text_delta", { text: "world" });
299
180
 
300
- expect(mockRpc).toHaveBeenCalledWith(
301
- "mcp_emit_event",
181
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
182
+ "event.emit",
183
+ expect.objectContaining({
184
+ message_id: "msg-001",
185
+ event_type: "text_delta",
186
+ seq: 1,
187
+ })
188
+ );
189
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
190
+ "event.emit",
302
191
  expect.objectContaining({
303
- p_message_id: "msg-001",
304
- p_event_type: "text_delta",
192
+ seq: 2,
305
193
  })
306
194
  );
307
195
  });
308
196
  });
309
197
 
310
198
  describe("loginWithToken()", () => {
311
- it("validates am_ token via RPC and returns user ID", async () => {
312
- setResult({ data: [{ out_user_id: "user-123" }], error: null });
199
+ it("validates am_ token via edge function and returns user ID", async () => {
200
+ mockCallMcpHandler.mockResolvedValueOnce({ user_id: "user-123", email: null });
313
201
 
314
202
  const userId = await loginWithToken("am_valid_test_token");
315
203
  expect(userId).toBe("user-123");
316
- expect(mockRpc).toHaveBeenCalledWith(
317
- "validate_mcp_token",
318
- expect.objectContaining({
319
- p_token_hash: expect.any(String),
320
- })
204
+ expect(mockCallMcpHandler).toHaveBeenCalledWith(
205
+ "auth.validate_token",
206
+ {},
207
+ "am_valid_test_token"
321
208
  );
322
209
  });
323
210
 
@@ -325,16 +212,16 @@ describe("Supabase DB Layer", () => {
325
212
  await expect(loginWithToken("not-am-token")).rejects.toThrow("Invalid token format");
326
213
  });
327
214
 
328
- it("throws on RPC validation failure", async () => {
329
- setResult({ data: null, error: { message: "validation failed" } });
215
+ it("throws on edge function validation failure", async () => {
216
+ mockCallMcpHandler.mockRejectedValueOnce(new Error("Token validation failed"));
330
217
 
331
218
  await expect(loginWithToken("am_bad_token")).rejects.toThrow("Token validation failed");
332
219
  });
333
220
  });
334
221
 
335
222
  describe("getCurrentUserId()", () => {
336
- it("returns user ID when authenticated", async () => {
337
- setResult({ data: [{ out_user_id: "user-123" }], error: null });
223
+ it("returns user ID from auth.validate_token", async () => {
224
+ mockCallMcpHandler.mockResolvedValueOnce({ user_id: "user-123" });
338
225
  const userId = await getCurrentUserId();
339
226
  expect(userId).toBe("user-123");
340
227
  });