assistme 0.3.0 → 0.3.2

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.
Files changed (44) hide show
  1. package/PLAN.md +14 -3
  2. package/dist/{chunk-UWE5WVQI.js → chunk-KX7ITO55.js} +20 -11
  3. package/dist/index.js +1791 -572
  4. package/dist/{job-runner-N4XAAWLJ.js → job-runner-P2L6MOOX.js} +1 -1
  5. package/package.json +5 -3
  6. package/src/agent/job-runner.ts +9 -13
  7. package/src/agent/mcp-servers.ts +6 -1020
  8. package/src/agent/memory.ts +2 -11
  9. package/src/agent/processor.ts +18 -108
  10. package/src/agent/scheduler.ts +2 -3
  11. package/src/agent/session.ts +20 -36
  12. package/src/agent/skills.ts +167 -61
  13. package/src/agent/system-prompt.ts +126 -0
  14. package/src/browser/chrome-launcher.ts +555 -0
  15. package/src/browser/controller.ts +1386 -0
  16. package/src/browser/types.ts +70 -0
  17. package/src/commands/credential.ts +190 -0
  18. package/src/commands/job.ts +14 -45
  19. package/src/commands/memory.ts +16 -29
  20. package/src/commands/schedule.ts +15 -37
  21. package/src/commands/start.ts +11 -43
  22. package/src/credentials/credential-store.test.ts +162 -0
  23. package/src/credentials/credential-store.ts +266 -0
  24. package/src/credentials/encryption.test.ts +98 -0
  25. package/src/credentials/encryption.ts +82 -0
  26. package/src/credentials/index.ts +15 -0
  27. package/src/credentials/local-store.ts +89 -0
  28. package/src/db/action.ts +19 -0
  29. package/src/db/api-client.ts +3 -32
  30. package/src/db/auth-store.ts +41 -0
  31. package/src/db/auth.ts +38 -0
  32. package/src/db/conversation.ts +39 -0
  33. package/src/db/event.ts +52 -0
  34. package/src/db/job-poll.ts +18 -0
  35. package/src/db/session.ts +60 -0
  36. package/src/db/supabase.ts +40 -383
  37. package/src/db/task.ts +69 -0
  38. package/src/db/types.ts +54 -0
  39. package/src/index.ts +2 -0
  40. package/src/mcp/agent-tools-server.ts +1047 -0
  41. package/src/mcp/browser-server.ts +258 -0
  42. package/src/tools/browser.ts +28 -1208
  43. package/src/tools/index.ts +32 -263
  44. package/src/tools/web.ts +0 -73
@@ -0,0 +1,89 @@
1
+ import Database, { type Database as DatabaseType } from "better-sqlite3";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ const DEFAULT_DB_DIR = join(homedir(), ".config", "assistme");
7
+ const DEFAULT_DB_NAME = "local.db";
8
+
9
+ /**
10
+ * Singleton SQLite-backed local store.
11
+ *
12
+ * Provides a shared database for all local-only data:
13
+ * credentials, cached tokens, user preferences, etc.
14
+ *
15
+ * - Single file: ~/.config/assistme/local.db
16
+ * - WAL mode for concurrent read performance
17
+ * - Auto-creates tables on first access
18
+ * - Directory permissions: 0o700, file inherits from SQLite
19
+ */
20
+ export class LocalStore {
21
+ private db: DatabaseType;
22
+ readonly dbPath: string;
23
+
24
+ constructor(dbPath?: string) {
25
+ const dir = dbPath ? dbPath : DEFAULT_DB_DIR;
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
28
+ }
29
+
30
+ this.dbPath = dbPath
31
+ ? join(dbPath, DEFAULT_DB_NAME)
32
+ : join(DEFAULT_DB_DIR, DEFAULT_DB_NAME);
33
+
34
+ this.db = new Database(this.dbPath);
35
+ this.db.pragma("journal_mode = WAL");
36
+ this.db.pragma("foreign_keys = ON");
37
+
38
+ this.migrate();
39
+ }
40
+
41
+ /** Run schema migrations. Idempotent — safe to call on every startup. */
42
+ private migrate(): void {
43
+ this.db.exec(`
44
+ CREATE TABLE IF NOT EXISTS credentials (
45
+ id TEXT PRIMARY KEY,
46
+ name TEXT NOT NULL UNIQUE,
47
+ type TEXT NOT NULL DEFAULT 'secret',
48
+ skill_name TEXT,
49
+ tags TEXT NOT NULL DEFAULT '[]',
50
+ encrypted_data TEXT NOT NULL,
51
+ created_at TEXT NOT NULL,
52
+ updated_at TEXT NOT NULL
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_credentials_name ON credentials(name);
56
+ CREATE INDEX IF NOT EXISTS idx_credentials_skill ON credentials(skill_name);
57
+ CREATE INDEX IF NOT EXISTS idx_credentials_type ON credentials(type);
58
+ `);
59
+ }
60
+
61
+ /** Get the raw database handle for direct queries. */
62
+ getDb(): DatabaseType {
63
+ return this.db;
64
+ }
65
+
66
+ /** Close the database connection. */
67
+ close(): void {
68
+ this.db.close();
69
+ }
70
+ }
71
+
72
+ // ── Singleton ────────────────────────────────────────────────────────
73
+
74
+ let _instance: LocalStore | null = null;
75
+
76
+ export function getLocalStore(dbPath?: string): LocalStore {
77
+ if (!_instance) {
78
+ _instance = new LocalStore(dbPath);
79
+ }
80
+ return _instance;
81
+ }
82
+
83
+ /** Reset singleton (for tests). */
84
+ export function resetLocalStore(): void {
85
+ if (_instance) {
86
+ _instance.close();
87
+ _instance = null;
88
+ }
89
+ }
@@ -0,0 +1,19 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+
3
+ export async function setActionRequest(
4
+ messageId: string,
5
+ actionData: Record<string, unknown>
6
+ ): Promise<void> {
7
+ await callMcpHandler("action.set_request", {
8
+ message_id: messageId,
9
+ action_data: actionData,
10
+ });
11
+ }
12
+
13
+ export async function pollActionResponse(
14
+ messageId: string
15
+ ): Promise<Record<string, unknown> | null> {
16
+ return callMcpHandler<Record<string, unknown> | null>("action.poll_response", {
17
+ message_id: messageId,
18
+ });
19
+ }
@@ -1,36 +1,7 @@
1
1
  import { getConfig } from "../utils/config.js";
2
- import { existsSync, readFileSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
2
+ import { getRawToken } from "./auth-store.js";
5
3
 
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
- }
4
+ export { getRawToken };
34
5
 
35
6
  /**
36
7
  * Call the mcp-handler edge function.
@@ -42,7 +13,7 @@ export function getRawToken(): string {
42
13
  export async function callMcpHandler<T = unknown>(
43
14
  action: string,
44
15
  params: Record<string, unknown> = {},
45
- overrideToken?: string,
16
+ overrideToken?: string
46
17
  ): Promise<T> {
47
18
  const config = getConfig();
48
19
  const token = overrideToken || getRawToken();
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const AUTH_DIR = join(homedir(), ".config", "assistme");
6
+ const AUTH_FILE = join(AUTH_DIR, "auth.json");
7
+
8
+ export function ensureAuthDir(): void {
9
+ if (!existsSync(AUTH_DIR)) {
10
+ mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
11
+ }
12
+ }
13
+
14
+ export function readAuthStore(): Record<string, string> {
15
+ try {
16
+ if (existsSync(AUTH_FILE)) {
17
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
18
+ }
19
+ } catch {
20
+ // Corrupted file — start fresh
21
+ }
22
+ return {};
23
+ }
24
+
25
+ export function writeAuthStore(data: Record<string, string>): void {
26
+ ensureAuthDir();
27
+ writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
28
+ }
29
+
30
+ /**
31
+ * Get the raw MCP token from the local auth store.
32
+ * Throws if not authenticated.
33
+ */
34
+ export function getRawToken(): string {
35
+ const store = readAuthStore();
36
+ const token = store["mcp_token"];
37
+ if (!token || !token.startsWith("am_")) {
38
+ throw new Error("Not authenticated. Run `assistme login`.");
39
+ }
40
+ return token;
41
+ }
package/src/db/auth.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { readAuthStore, writeAuthStore } from "./auth-store.js";
2
+ import { callMcpHandler } from "./api-client.js";
3
+
4
+ /**
5
+ * Login using an am_ MCP token.
6
+ * Validates against DB via edge function, stores locally.
7
+ */
8
+ export async function loginWithToken(mcpToken: string): Promise<string> {
9
+ if (!mcpToken.startsWith("am_")) {
10
+ throw new Error("Invalid token format. Use an am_ token from the web page.");
11
+ }
12
+
13
+ const result = await callMcpHandler<{ user_id: string; email: string | null }>(
14
+ "auth.validate_token",
15
+ {},
16
+ mcpToken
17
+ );
18
+
19
+ // Persist token
20
+ const store = readAuthStore();
21
+ store["mcp_token"] = mcpToken;
22
+ writeAuthStore(store);
23
+
24
+ return result.user_id;
25
+ }
26
+
27
+ export async function getCurrentUserId(): Promise<string> {
28
+ const result = await callMcpHandler<{ user_id: string }>("auth.validate_token");
29
+ return result.user_id;
30
+ }
31
+
32
+ export async function logout(): Promise<void> {
33
+ try {
34
+ writeAuthStore({});
35
+ } catch {
36
+ // ignore
37
+ }
38
+ }
@@ -0,0 +1,39 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+ import type { HistoryEntry } from "./types.js";
4
+
5
+ export async function getOrCreateCliConversation(): Promise<string> {
6
+ const data = await callMcpHandler<string>("conversation.get_or_create");
7
+ return data;
8
+ }
9
+
10
+ /**
11
+ * Fetch completed messages from a conversation to build history context.
12
+ * Returns messages in chronological order (oldest first).
13
+ */
14
+ export async function getConversationHistory(
15
+ conversationId: string,
16
+ excludeMessageId: string,
17
+ limit: number = 20
18
+ ): Promise<HistoryEntry[]> {
19
+ try {
20
+ const rows = await callMcpHandler<Array<Record<string, unknown>>>("conversation.get_history", {
21
+ conversation_id: conversationId,
22
+ exclude_message_id: excludeMessageId,
23
+ limit,
24
+ });
25
+
26
+ return (rows || [])
27
+ .reverse() // chronological order (oldest first)
28
+ .map((row) => {
29
+ const prompt = ((row.metadata as Record<string, unknown>)?.prompt as string) || "";
30
+ const content = (row.content as string) || "";
31
+ const response = row.status === "failed" ? `[Task failed] ${content}` : content;
32
+ return { prompt, response };
33
+ })
34
+ .filter((entry) => entry.prompt && entry.response);
35
+ } catch (err) {
36
+ log.debug(`Failed to fetch conversation history: ${err instanceof Error ? err.message : err}`);
37
+ return [];
38
+ }
39
+ }
@@ -0,0 +1,52 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+ import type { EventType } from "./types.js";
4
+
5
+ /**
6
+ * Per-task event emitter. Each task gets its own sequence counter
7
+ * to avoid cross-task sequence number collisions.
8
+ */
9
+ export class TaskEventEmitter {
10
+ private sequence = 0;
11
+
12
+ constructor(private messageId: string) {}
13
+
14
+ async emit(eventType: EventType, eventData: Record<string, unknown>): Promise<void> {
15
+ this.sequence++;
16
+ try {
17
+ await callMcpHandler("event.emit", {
18
+ message_id: this.messageId,
19
+ event_type: eventType,
20
+ event_data: eventData,
21
+ seq: this.sequence,
22
+ });
23
+ } catch (err) {
24
+ log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
25
+ }
26
+ }
27
+ }
28
+
29
+ // Legacy global API — kept for backward compatibility during migration
30
+ let eventSequence = 0;
31
+
32
+ export function resetEventSequence(): void {
33
+ eventSequence = 0;
34
+ }
35
+
36
+ export async function emitEvent(
37
+ messageId: string,
38
+ eventType: EventType,
39
+ eventData: Record<string, unknown>
40
+ ): Promise<void> {
41
+ eventSequence++;
42
+ try {
43
+ await callMcpHandler("event.emit", {
44
+ message_id: messageId,
45
+ event_type: eventType,
46
+ event_data: eventData,
47
+ seq: eventSequence,
48
+ });
49
+ } catch (err) {
50
+ log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
51
+ }
52
+ }
@@ -0,0 +1,18 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+ import type { PendingJobRun } from "./types.js";
4
+
5
+ /**
6
+ * Atomically claim one pending job run.
7
+ * Uses FOR UPDATE SKIP LOCKED — concurrent CLIs will never grab the same run.
8
+ * Returns null if no pending job run exists.
9
+ */
10
+ export async function pollAndClaimJobRun(): Promise<PendingJobRun | null> {
11
+ try {
12
+ const data = await callMcpHandler<PendingJobRun | null>("job.claim_pending_run");
13
+ return data;
14
+ } catch (err) {
15
+ log.debug(`Job run poll failed: ${err instanceof Error ? err.message : err}`);
16
+ return null;
17
+ }
18
+ }
@@ -0,0 +1,60 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+ import type { AgentSession } from "./types.js";
4
+
5
+ export async function createSession(
6
+ sessionName: string,
7
+ workspacePath: string,
8
+ version: string
9
+ ): Promise<AgentSession> {
10
+ const { getConfig } = await import("../utils/config.js");
11
+ const data = await callMcpHandler<AgentSession>("session.create", {
12
+ session_name: sessionName,
13
+ workspace_path: workspacePath,
14
+ version,
15
+ model: getConfig().model || null,
16
+ });
17
+ return data;
18
+ }
19
+
20
+ export async function updateHeartbeat(sessionId: string): Promise<void> {
21
+ try {
22
+ await callMcpHandler("session.heartbeat", { session_id: sessionId });
23
+ } catch (err) {
24
+ log.warn(`Heartbeat update failed: ${err instanceof Error ? err.message : err}`);
25
+ }
26
+ }
27
+
28
+ export async function endSession(sessionId: string): Promise<void> {
29
+ try {
30
+ await callMcpHandler("session.end", { session_id: sessionId });
31
+ } catch (err) {
32
+ log.error(`Failed to end session: ${err instanceof Error ? err.message : err}`);
33
+ }
34
+ }
35
+
36
+ export async function setSessionBusy(sessionId: string, busy: boolean): Promise<void> {
37
+ await callMcpHandler("session.set_busy", {
38
+ session_id: sessionId,
39
+ busy,
40
+ });
41
+ }
42
+
43
+ export async function cleanupStaleSessions(
44
+ currentSessionId: string,
45
+ thresholdMs = 120_000
46
+ ): Promise<number> {
47
+ try {
48
+ const result = await callMcpHandler<{ cleaned: number }>("session.cleanup_stale", {
49
+ current_session_id: currentSessionId,
50
+ threshold_ms: thresholdMs,
51
+ });
52
+ return result.cleaned;
53
+ } catch {
54
+ return 0;
55
+ }
56
+ }
57
+
58
+ export async function getActiveSessions(limit = 5): Promise<AgentSession[]> {
59
+ return callMcpHandler<AgentSession[]>("session.get_active", { limit });
60
+ }