@emqo/claudebridge 0.8.0 → 0.9.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.
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ declare const EndpointSchema: z.ZodObject<{
3
+ name: z.ZodDefault<z.ZodString>;
4
+ model: z.ZodDefault<z.ZodString>;
5
+ provider: z.ZodDefault<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ declare const MemoryConfigSchema: z.ZodObject<{
8
+ enabled: z.ZodDefault<z.ZodBoolean>;
9
+ auto_summary: z.ZodDefault<z.ZodBoolean>;
10
+ max_memories: z.ZodDefault<z.ZodNumber>;
11
+ }, z.core.$strip>;
12
+ declare const SkillConfigSchema: z.ZodObject<{
13
+ enabled: z.ZodDefault<z.ZodBoolean>;
14
+ }, z.core.$strip>;
15
+ declare const SessionConfigSchema: z.ZodObject<{
16
+ enabled: z.ZodDefault<z.ZodBoolean>;
17
+ max_per_user: z.ZodDefault<z.ZodNumber>;
18
+ idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
19
+ classifier_budget: z.ZodDefault<z.ZodNumber>;
20
+ classifier_model: z.ZodDefault<z.ZodString>;
21
+ }, z.core.$strip>;
22
+ declare const AgentConfigSchema: z.ZodObject<{
23
+ allowed_tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
+ permission_mode: z.ZodDefault<z.ZodString>;
25
+ max_turns: z.ZodDefault<z.ZodNumber>;
26
+ max_budget_usd: z.ZodDefault<z.ZodNumber>;
27
+ system_prompt: z.ZodDefault<z.ZodString>;
28
+ cwd: z.ZodDefault<z.ZodString>;
29
+ timeout_seconds: z.ZodDefault<z.ZodNumber>;
30
+ max_parallel: z.ZodDefault<z.ZodNumber>;
31
+ memory: z.ZodDefault<z.ZodObject<{
32
+ enabled: z.ZodDefault<z.ZodBoolean>;
33
+ auto_summary: z.ZodDefault<z.ZodBoolean>;
34
+ max_memories: z.ZodDefault<z.ZodNumber>;
35
+ }, z.core.$strip>>;
36
+ skill: z.ZodDefault<z.ZodObject<{
37
+ enabled: z.ZodDefault<z.ZodBoolean>;
38
+ }, z.core.$strip>>;
39
+ session: z.ZodDefault<z.ZodObject<{
40
+ enabled: z.ZodDefault<z.ZodBoolean>;
41
+ max_per_user: z.ZodDefault<z.ZodNumber>;
42
+ idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
43
+ classifier_budget: z.ZodDefault<z.ZodNumber>;
44
+ classifier_model: z.ZodDefault<z.ZodString>;
45
+ }, z.core.$strip>>;
46
+ }, z.core.$strip>;
47
+ declare const WorkspaceConfigSchema: z.ZodObject<{
48
+ base_dir: z.ZodDefault<z.ZodString>;
49
+ isolation: z.ZodDefault<z.ZodBoolean>;
50
+ }, z.core.$strip>;
51
+ declare const AccessConfigSchema: z.ZodObject<{
52
+ allowed_users: z.ZodDefault<z.ZodArray<z.ZodString>>;
53
+ allowed_groups: z.ZodDefault<z.ZodArray<z.ZodString>>;
54
+ }, z.core.$strip>;
55
+ declare const TelegramConfigSchema: z.ZodObject<{
56
+ enabled: z.ZodDefault<z.ZodBoolean>;
57
+ token: z.ZodDefault<z.ZodString>;
58
+ chunk_size: z.ZodDefault<z.ZodNumber>;
59
+ }, z.core.$strip>;
60
+ declare const DiscordConfigSchema: z.ZodObject<{
61
+ enabled: z.ZodDefault<z.ZodBoolean>;
62
+ token: z.ZodDefault<z.ZodString>;
63
+ chunk_size: z.ZodDefault<z.ZodNumber>;
64
+ }, z.core.$strip>;
65
+ declare const RedisConfigSchema: z.ZodObject<{
66
+ enabled: z.ZodDefault<z.ZodBoolean>;
67
+ url: z.ZodDefault<z.ZodString>;
68
+ }, z.core.$strip>;
69
+ declare const WebhookConfigSchema: z.ZodObject<{
70
+ enabled: z.ZodDefault<z.ZodBoolean>;
71
+ port: z.ZodDefault<z.ZodNumber>;
72
+ token: z.ZodDefault<z.ZodString>;
73
+ github_secret: z.ZodDefault<z.ZodString>;
74
+ }, z.core.$strip>;
75
+ declare const CronEntrySchema: z.ZodObject<{
76
+ schedule_minutes: z.ZodNumber;
77
+ user_id: z.ZodString;
78
+ platform: z.ZodString;
79
+ chat_id: z.ZodString;
80
+ description: z.ZodString;
81
+ }, z.core.$strip>;
82
+ export declare const ConfigSchema: z.ZodObject<{
83
+ endpoints: z.ZodDefault<z.ZodArray<z.ZodObject<{
84
+ name: z.ZodDefault<z.ZodString>;
85
+ model: z.ZodDefault<z.ZodString>;
86
+ provider: z.ZodDefault<z.ZodString>;
87
+ }, z.core.$strip>>>;
88
+ log_level: z.ZodDefault<z.ZodString>;
89
+ locale: z.ZodDefault<z.ZodString>;
90
+ agent: z.ZodDefault<z.ZodObject<{
91
+ allowed_tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
92
+ permission_mode: z.ZodDefault<z.ZodString>;
93
+ max_turns: z.ZodDefault<z.ZodNumber>;
94
+ max_budget_usd: z.ZodDefault<z.ZodNumber>;
95
+ system_prompt: z.ZodDefault<z.ZodString>;
96
+ cwd: z.ZodDefault<z.ZodString>;
97
+ timeout_seconds: z.ZodDefault<z.ZodNumber>;
98
+ max_parallel: z.ZodDefault<z.ZodNumber>;
99
+ memory: z.ZodDefault<z.ZodObject<{
100
+ enabled: z.ZodDefault<z.ZodBoolean>;
101
+ auto_summary: z.ZodDefault<z.ZodBoolean>;
102
+ max_memories: z.ZodDefault<z.ZodNumber>;
103
+ }, z.core.$strip>>;
104
+ skill: z.ZodDefault<z.ZodObject<{
105
+ enabled: z.ZodDefault<z.ZodBoolean>;
106
+ }, z.core.$strip>>;
107
+ session: z.ZodDefault<z.ZodObject<{
108
+ enabled: z.ZodDefault<z.ZodBoolean>;
109
+ max_per_user: z.ZodDefault<z.ZodNumber>;
110
+ idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
111
+ classifier_budget: z.ZodDefault<z.ZodNumber>;
112
+ classifier_model: z.ZodDefault<z.ZodString>;
113
+ }, z.core.$strip>>;
114
+ }, z.core.$strip>>;
115
+ workspace: z.ZodDefault<z.ZodObject<{
116
+ base_dir: z.ZodDefault<z.ZodString>;
117
+ isolation: z.ZodDefault<z.ZodBoolean>;
118
+ }, z.core.$strip>>;
119
+ access: z.ZodDefault<z.ZodObject<{
120
+ allowed_users: z.ZodDefault<z.ZodArray<z.ZodString>>;
121
+ allowed_groups: z.ZodDefault<z.ZodArray<z.ZodString>>;
122
+ }, z.core.$strip>>;
123
+ redis: z.ZodDefault<z.ZodObject<{
124
+ enabled: z.ZodDefault<z.ZodBoolean>;
125
+ url: z.ZodDefault<z.ZodString>;
126
+ }, z.core.$strip>>;
127
+ platforms: z.ZodDefault<z.ZodObject<{
128
+ telegram: z.ZodDefault<z.ZodObject<{
129
+ enabled: z.ZodDefault<z.ZodBoolean>;
130
+ token: z.ZodDefault<z.ZodString>;
131
+ chunk_size: z.ZodDefault<z.ZodNumber>;
132
+ }, z.core.$strip>>;
133
+ discord: z.ZodDefault<z.ZodObject<{
134
+ enabled: z.ZodDefault<z.ZodBoolean>;
135
+ token: z.ZodDefault<z.ZodString>;
136
+ chunk_size: z.ZodDefault<z.ZodNumber>;
137
+ }, z.core.$strip>>;
138
+ }, z.core.$strip>>;
139
+ webhook: z.ZodDefault<z.ZodObject<{
140
+ enabled: z.ZodDefault<z.ZodBoolean>;
141
+ port: z.ZodDefault<z.ZodNumber>;
142
+ token: z.ZodDefault<z.ZodString>;
143
+ github_secret: z.ZodDefault<z.ZodString>;
144
+ }, z.core.$strip>>;
145
+ cron: z.ZodDefault<z.ZodArray<z.ZodObject<{
146
+ schedule_minutes: z.ZodNumber;
147
+ user_id: z.ZodString;
148
+ platform: z.ZodString;
149
+ chat_id: z.ZodString;
150
+ description: z.ZodString;
151
+ }, z.core.$strip>>>;
152
+ }, z.core.$strip>;
153
+ export type Config = z.infer<typeof ConfigSchema>;
154
+ export type Endpoint = z.infer<typeof EndpointSchema>;
155
+ export type AgentConfig = z.infer<typeof AgentConfigSchema>;
156
+ export type MemoryConfig = z.infer<typeof MemoryConfigSchema>;
157
+ export type SkillConfig = z.infer<typeof SkillConfigSchema>;
158
+ export type SessionConfig = z.infer<typeof SessionConfigSchema>;
159
+ export type WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>;
160
+ export type AccessConfig = z.infer<typeof AccessConfigSchema>;
161
+ export type TelegramConfig = z.infer<typeof TelegramConfigSchema>;
162
+ export type DiscordConfig = z.infer<typeof DiscordConfigSchema>;
163
+ export type RedisConfig = z.infer<typeof RedisConfigSchema>;
164
+ export type WebhookConfig = z.infer<typeof WebhookConfigSchema>;
165
+ export type CronEntry = z.infer<typeof CronEntrySchema>;
166
+ export {};
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ const EndpointSchema = z.object({
3
+ name: z.string().default("default"),
4
+ model: z.string().default(""),
5
+ provider: z.string().default("claude"),
6
+ });
7
+ const MemoryConfigSchema = z.object({
8
+ enabled: z.boolean().default(true),
9
+ auto_summary: z.boolean().default(true),
10
+ max_memories: z.number().int().positive().default(50),
11
+ });
12
+ const SkillConfigSchema = z.object({
13
+ enabled: z.boolean().default(true),
14
+ });
15
+ const SessionConfigSchema = z.object({
16
+ enabled: z.boolean().default(true),
17
+ max_per_user: z.number().int().positive().default(3),
18
+ idle_timeout_minutes: z.number().positive().default(30),
19
+ classifier_budget: z.number().nonnegative().default(0.05),
20
+ classifier_model: z.string().default(""),
21
+ });
22
+ const AgentConfigSchema = z.object({
23
+ allowed_tools: z.array(z.string()).default([]),
24
+ permission_mode: z.string().default("acceptEdits"),
25
+ max_turns: z.number().int().positive().default(50),
26
+ max_budget_usd: z.number().nonnegative().default(2.0),
27
+ system_prompt: z.string().default(""),
28
+ cwd: z.string().default(""),
29
+ timeout_seconds: z.number().int().nonnegative().default(0),
30
+ max_parallel: z.number().int().positive().default(1),
31
+ memory: MemoryConfigSchema.default(() => ({})),
32
+ skill: SkillConfigSchema.default(() => ({})),
33
+ session: SessionConfigSchema.default(() => ({})),
34
+ });
35
+ const WorkspaceConfigSchema = z.object({
36
+ base_dir: z.string().default("./workspaces"),
37
+ isolation: z.boolean().default(true),
38
+ });
39
+ const AccessConfigSchema = z.object({
40
+ allowed_users: z.array(z.string()).default([]),
41
+ allowed_groups: z.array(z.string()).default([]),
42
+ });
43
+ const TelegramConfigSchema = z.object({
44
+ enabled: z.boolean().default(true),
45
+ token: z.string().default(""),
46
+ chunk_size: z.number().int().positive().default(4000),
47
+ });
48
+ const DiscordConfigSchema = z.object({
49
+ enabled: z.boolean().default(false),
50
+ token: z.string().default(""),
51
+ chunk_size: z.number().int().positive().default(1900),
52
+ });
53
+ const RedisConfigSchema = z.object({
54
+ enabled: z.boolean().default(false),
55
+ url: z.string().default(""),
56
+ });
57
+ const WebhookConfigSchema = z.object({
58
+ enabled: z.boolean().default(false),
59
+ port: z.number().int().positive().default(3100),
60
+ token: z.string().default(""),
61
+ github_secret: z.string().default(""),
62
+ });
63
+ const CronEntrySchema = z.object({
64
+ schedule_minutes: z.number().int().positive(),
65
+ user_id: z.string(),
66
+ platform: z.string(),
67
+ chat_id: z.string(),
68
+ description: z.string(),
69
+ });
70
+ const PlatformsSchema = z.object({
71
+ telegram: TelegramConfigSchema.default(() => ({})),
72
+ discord: DiscordConfigSchema.default(() => ({})),
73
+ });
74
+ export const ConfigSchema = z.object({
75
+ endpoints: z.array(EndpointSchema).default([]),
76
+ log_level: z.string().default("info"),
77
+ locale: z.string().default("en"),
78
+ agent: AgentConfigSchema.default(() => ({})),
79
+ workspace: WorkspaceConfigSchema.default(() => ({})),
80
+ access: AccessConfigSchema.default(() => ({})),
81
+ redis: RedisConfigSchema.default(() => ({})),
82
+ platforms: PlatformsSchema.default(() => ({})),
83
+ webhook: WebhookConfigSchema.default(() => ({})),
84
+ cron: z.array(CronEntrySchema).default([]),
85
+ });
@@ -0,0 +1,50 @@
1
+ import { Store } from "./store.js";
2
+ import { SessionConfig } from "./config.js";
3
+ export interface SubSession {
4
+ id: string;
5
+ userId: string;
6
+ platform: string;
7
+ chatId: string;
8
+ claudeSessionId: string | null;
9
+ label: string;
10
+ status: "active" | "idle" | "expired" | "closed";
11
+ createdAt: number;
12
+ lastActiveAt: number;
13
+ messageCount: number;
14
+ totalCost: number;
15
+ }
16
+ export declare class SessionManager {
17
+ private store;
18
+ private config;
19
+ constructor(store: Store, config: SessionConfig);
20
+ /** Create a new sub-session for the user */
21
+ create(userId: string, platform: string, chatId: string, label?: string): SubSession;
22
+ /** Get a sub-session by ID */
23
+ get(sessionId: string): SubSession | null;
24
+ /** Get all active (active/idle) sub-sessions for a user+platform */
25
+ getActive(userId: string, platform: string): SubSession[];
26
+ /** Update lastActiveAt and increment message count */
27
+ touch(sessionId: string): void;
28
+ /** Save the claude CLI session_id for resume */
29
+ setClaudeSessionId(sessionId: string, claudeId: string): void;
30
+ /** Update the topic label */
31
+ updateLabel(sessionId: string, label: string): void;
32
+ /** Add cost to a sub-session */
33
+ addCost(sessionId: string, cost: number): void;
34
+ /** Close a specific sub-session */
35
+ close(sessionId: string): void;
36
+ /** Close all active sub-sessions for a user (equivalent to /new) */
37
+ closeAll(userId: string): void;
38
+ /** Check if a user can create another sub-session (within limit) */
39
+ canCreate(userId: string, platform: string): boolean;
40
+ /** Expire idle sub-sessions and prune old message mappings. Call periodically. */
41
+ expireIdle(): number;
42
+ /** Track a platform message → sub-session mapping (for reply-to routing) */
43
+ trackMessage(platformMsgId: string, chatId: string, subSessionId: string): void;
44
+ /** Look up which sub-session a platform message belongs to */
45
+ getSessionByMessage(platformMsgId: string, chatId: string): string | null;
46
+ /** Check if a sub-session is usable (active or idle) */
47
+ isUsable(session: SubSession): boolean;
48
+ /** Get all sub-sessions for a user (all statuses) */
49
+ getAll(userId: string): SubSession[];
50
+ }
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from "crypto";
2
+ import { log as rootLog } from "./logger.js";
3
+ const log = rootLog.child("session");
4
+ /** Maps a DB row to a SubSession domain object */
5
+ function toSubSession(row) {
6
+ return {
7
+ id: row.id,
8
+ userId: row.user_id,
9
+ platform: row.platform,
10
+ chatId: row.chat_id,
11
+ claudeSessionId: row.claude_session_id ?? null,
12
+ label: row.label,
13
+ status: row.status,
14
+ createdAt: row.created_at,
15
+ lastActiveAt: row.last_active_at,
16
+ messageCount: row.message_count,
17
+ totalCost: row.total_cost,
18
+ };
19
+ }
20
+ export class SessionManager {
21
+ store;
22
+ config;
23
+ constructor(store, config) {
24
+ this.store = store;
25
+ this.config = config;
26
+ }
27
+ /** Create a new sub-session for the user */
28
+ create(userId, platform, chatId, label) {
29
+ const id = randomUUID();
30
+ const trimmedLabel = (label || "").slice(0, 50);
31
+ this.store.createSubSession(id, userId, platform, chatId, trimmedLabel);
32
+ const row = this.store.getSubSession(id);
33
+ return toSubSession(row);
34
+ }
35
+ /** Get a sub-session by ID */
36
+ get(sessionId) {
37
+ const row = this.store.getSubSession(sessionId);
38
+ return row ? toSubSession(row) : null;
39
+ }
40
+ /** Get all active (active/idle) sub-sessions for a user+platform */
41
+ getActive(userId, platform) {
42
+ return this.store.getActiveSubSessions(userId, platform).map(toSubSession);
43
+ }
44
+ /** Update lastActiveAt and increment message count */
45
+ touch(sessionId) {
46
+ this.store.touchSubSession(sessionId);
47
+ }
48
+ /** Save the claude CLI session_id for resume */
49
+ setClaudeSessionId(sessionId, claudeId) {
50
+ this.store.setSubSessionClaudeId(sessionId, claudeId);
51
+ }
52
+ /** Update the topic label */
53
+ updateLabel(sessionId, label) {
54
+ this.store.updateSubSessionLabel(sessionId, label.slice(0, 50));
55
+ }
56
+ /** Add cost to a sub-session */
57
+ addCost(sessionId, cost) {
58
+ if (cost > 0)
59
+ this.store.updateSubSessionCost(sessionId, cost);
60
+ }
61
+ /** Close a specific sub-session */
62
+ close(sessionId) {
63
+ this.store.closeSubSession(sessionId);
64
+ }
65
+ /** Close all active sub-sessions for a user (equivalent to /new) */
66
+ closeAll(userId) {
67
+ this.store.closeAllSubSessions(userId);
68
+ }
69
+ /** Check if a user can create another sub-session (within limit) */
70
+ canCreate(userId, platform) {
71
+ const active = this.store.getActiveSubSessions(userId, platform);
72
+ return active.length < this.config.max_per_user;
73
+ }
74
+ /** Expire idle sub-sessions and prune old message mappings. Call periodically. */
75
+ expireIdle() {
76
+ const timeoutMs = this.config.idle_timeout_minutes * 60 * 1000;
77
+ const expired = this.store.expireIdleSessions(timeoutMs);
78
+ // Also prune message mappings older than 24h
79
+ this.store.pruneSubSessionMessages(24 * 60 * 60 * 1000);
80
+ if (expired > 0)
81
+ log.info("expired idle sub-sessions", { count: expired });
82
+ return expired;
83
+ }
84
+ /** Track a platform message → sub-session mapping (for reply-to routing) */
85
+ trackMessage(platformMsgId, chatId, subSessionId) {
86
+ this.store.trackSubSessionMessage(platformMsgId, chatId, subSessionId);
87
+ }
88
+ /** Look up which sub-session a platform message belongs to */
89
+ getSessionByMessage(platformMsgId, chatId) {
90
+ return this.store.getSubSessionByMessage(platformMsgId, chatId);
91
+ }
92
+ /** Check if a sub-session is usable (active or idle) */
93
+ isUsable(session) {
94
+ return session.status === "active" || session.status === "idle";
95
+ }
96
+ /** Get all sub-sessions for a user (all statuses) */
97
+ getAll(userId) {
98
+ return this.store.getAllSubSessions(userId).map(toSubSession);
99
+ }
100
+ }
@@ -2,6 +2,8 @@ export declare class Store {
2
2
  private db;
3
3
  readonly dbPath: string;
4
4
  constructor(dbPath?: string);
5
+ private _migrateFromLegacySessions;
6
+ close(): void;
5
7
  getSession(userId: string): string | null;
6
8
  setSession(userId: string, sessionId: string, platform: string): void;
7
9
  clearSession(userId: string): void;
@@ -31,14 +33,6 @@ export declare class Store {
31
33
  clearMemories(userId: string): void;
32
34
  trimMemories(userId: string, max: number): void;
33
35
  addTask(userId: string, platform: string, chatId: string, description: string, remindAt?: number, auto?: boolean, parentId?: number, scheduledAt?: number): number;
34
- getTasks(userId: string): {
35
- id: number;
36
- description: string;
37
- status: string;
38
- remind_at: number | null;
39
- created_at: number;
40
- }[];
41
- completeTask(taskId: number, userId: string): boolean;
42
36
  getDueReminders(): {
43
37
  id: number;
44
38
  user_id: string;
@@ -47,13 +41,6 @@ export declare class Store {
47
41
  description: string;
48
42
  }[];
49
43
  markReminderSent(taskId: number): void;
50
- getNextAutoTask(platform?: string): {
51
- id: number;
52
- user_id: string;
53
- platform: string;
54
- chat_id: string;
55
- description: string;
56
- } | null;
57
44
  markTaskRunning(taskId: number): void;
58
45
  markTaskResult(taskId: number, status: string): void;
59
46
  getAutoTasks(userId: string): {
@@ -108,4 +95,54 @@ export declare class Store {
108
95
  scheduled_at: number | null;
109
96
  created_at: number;
110
97
  }[];
98
+ createSubSession(id: string, userId: string, platform: string, chatId: string, label: string): void;
99
+ getSubSession(id: string): {
100
+ id: string;
101
+ user_id: string;
102
+ platform: string;
103
+ chat_id: string;
104
+ claude_session_id: string | null;
105
+ label: string;
106
+ status: string;
107
+ created_at: number;
108
+ last_active_at: number;
109
+ message_count: number;
110
+ total_cost: number;
111
+ } | null;
112
+ getActiveSubSessions(userId: string, platform: string): {
113
+ id: string;
114
+ user_id: string;
115
+ platform: string;
116
+ chat_id: string;
117
+ claude_session_id: string | null;
118
+ label: string;
119
+ status: string;
120
+ created_at: number;
121
+ last_active_at: number;
122
+ message_count: number;
123
+ total_cost: number;
124
+ }[];
125
+ touchSubSession(id: string): void;
126
+ setSubSessionClaudeId(id: string, claudeSessionId: string): void;
127
+ updateSubSessionLabel(id: string, label: string): void;
128
+ updateSubSessionCost(id: string, cost: number): void;
129
+ closeSubSession(id: string): void;
130
+ closeAllSubSessions(userId: string): void;
131
+ expireIdleSessions(timeoutMs: number): number;
132
+ trackSubSessionMessage(platformMsgId: string, chatId: string, subSessionId: string): void;
133
+ getSubSessionByMessage(platformMsgId: string, chatId: string): string | null;
134
+ pruneSubSessionMessages(maxAgeMs: number): number;
135
+ getAllSubSessions(userId: string): {
136
+ id: string;
137
+ user_id: string;
138
+ platform: string;
139
+ chat_id: string;
140
+ claude_session_id: string | null;
141
+ label: string;
142
+ status: string;
143
+ created_at: number;
144
+ last_active_at: number;
145
+ message_count: number;
146
+ total_cost: number;
147
+ }[];
111
148
  }
@@ -1,13 +1,17 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { mkdirSync } from "fs";
3
3
  import { dirname, resolve } from "path";
4
+ import { log as rootLog } from "./logger.js";
5
+ const log = rootLog.child("store");
4
6
  const DEFAULT_DB_PATH = "./data/claudebridge.db";
5
7
  export class Store {
6
8
  db;
7
9
  dbPath;
8
10
  constructor(dbPath) {
9
- this.dbPath = resolve(dbPath || DEFAULT_DB_PATH);
10
- mkdirSync(dirname(this.dbPath), { recursive: true });
11
+ const p = dbPath || DEFAULT_DB_PATH;
12
+ this.dbPath = p === ":memory:" ? p : resolve(p);
13
+ if (p !== ":memory:")
14
+ mkdirSync(dirname(this.dbPath), { recursive: true });
11
15
  this.db = new Database(this.dbPath);
12
16
  this.db.pragma("journal_mode = WAL");
13
17
  this.db.exec(`
@@ -54,6 +58,27 @@ export class Store {
54
58
  );
55
59
  CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id);
56
60
  CREATE INDEX IF NOT EXISTS idx_tasks_user ON tasks(user_id, status);
61
+ CREATE TABLE IF NOT EXISTS sub_sessions (
62
+ id TEXT PRIMARY KEY,
63
+ user_id TEXT NOT NULL,
64
+ platform TEXT NOT NULL,
65
+ chat_id TEXT NOT NULL,
66
+ claude_session_id TEXT,
67
+ label TEXT NOT NULL DEFAULT '',
68
+ status TEXT NOT NULL DEFAULT 'active',
69
+ created_at INTEGER NOT NULL,
70
+ last_active_at INTEGER NOT NULL,
71
+ message_count INTEGER NOT NULL DEFAULT 0,
72
+ total_cost REAL NOT NULL DEFAULT 0
73
+ );
74
+ CREATE INDEX IF NOT EXISTS idx_subsess_user ON sub_sessions(user_id, platform, status);
75
+ CREATE TABLE IF NOT EXISTS sub_session_messages (
76
+ platform_msg_id TEXT NOT NULL,
77
+ chat_id TEXT NOT NULL,
78
+ sub_session_id TEXT NOT NULL,
79
+ created_at INTEGER NOT NULL,
80
+ PRIMARY KEY (platform_msg_id, chat_id)
81
+ );
57
82
  `);
58
83
  // Schema migration: add parent_id, result, and scheduled_at columns
59
84
  try {
@@ -76,12 +101,30 @@ export class Store {
76
101
  this.db.prepare("UPDATE tasks SET status = 'auto', description = ? WHERE id = ?").run(desc, t.id);
77
102
  }
78
103
  if (orphaned.length > 0) {
79
- console.log(`[store] recovered ${orphaned.length} orphaned running task(s) back to auto queue`);
104
+ log.info("recovered orphaned running tasks", { count: orphaned.length });
80
105
  }
81
106
  // Startup cleanup: prune history/usage older than 30 days
82
107
  const cutoff = Date.now() - 30 * 86400000;
83
108
  this.db.prepare("DELETE FROM history WHERE created_at < ?").run(cutoff);
84
109
  this.db.prepare("DELETE FROM usage WHERE created_at < ?").run(cutoff);
110
+ // Migrate legacy sessions → sub_sessions (one-time)
111
+ this._migrateFromLegacySessions();
112
+ }
113
+ _migrateFromLegacySessions() {
114
+ const legacyCount = this.db.prepare("SELECT COUNT(*) as c FROM sessions").get().c;
115
+ const subCount = this.db.prepare("SELECT COUNT(*) as c FROM sub_sessions").get().c;
116
+ if (legacyCount > 0 && subCount === 0) {
117
+ const rows = this.db.prepare("SELECT user_id, session_id, platform, updated_at FROM sessions").all();
118
+ const crypto = require("crypto");
119
+ for (const row of rows) {
120
+ const id = crypto.randomUUID();
121
+ this.db.prepare("INSERT INTO sub_sessions (id, user_id, platform, chat_id, claude_session_id, label, status, created_at, last_active_at, message_count, total_cost) VALUES (?, ?, ?, '', ?, 'main', 'active', ?, ?, 1, 0)").run(id, row.user_id, row.platform, row.session_id, row.updated_at, row.updated_at);
122
+ }
123
+ log.info("migrated legacy sessions to sub_sessions", { count: rows.length });
124
+ }
125
+ }
126
+ close() {
127
+ this.db.close();
85
128
  }
86
129
  // --- sessions ---
87
130
  getSession(userId) {
@@ -151,26 +194,12 @@ export class Store {
151
194
  const r = this.db.prepare("INSERT INTO tasks (user_id, platform, chat_id, description, status, remind_at, parent_id, scheduled_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(userId, platform, chatId, description, auto ? "auto" : "pending", remindAt ?? null, parentId ?? null, scheduledAt ?? null, Date.now());
152
195
  return Number(r.lastInsertRowid);
153
196
  }
154
- getTasks(userId) {
155
- return this.db.prepare("SELECT id, description, status, remind_at, created_at FROM tasks WHERE user_id = ? AND status = 'pending' ORDER BY created_at DESC").all(userId);
156
- }
157
- completeTask(taskId, userId) {
158
- const r = this.db.prepare("UPDATE tasks SET status = 'done' WHERE id = ? AND user_id = ? AND status = 'pending'").run(taskId, userId);
159
- return r.changes > 0;
160
- }
161
197
  getDueReminders() {
162
198
  return this.db.prepare("SELECT id, user_id, platform, chat_id, description FROM tasks WHERE status = 'pending' AND remind_at IS NOT NULL AND remind_at <= ? AND reminder_sent = 0").all(Date.now());
163
199
  }
164
200
  markReminderSent(taskId) {
165
201
  this.db.prepare("UPDATE tasks SET reminder_sent = 1 WHERE id = ?").run(taskId);
166
202
  }
167
- getNextAutoTask(platform) {
168
- const now = Date.now();
169
- if (platform) {
170
- return this.db.prepare("SELECT id, user_id, platform, chat_id, description FROM tasks WHERE status = 'auto' AND platform = ? AND (scheduled_at IS NULL OR scheduled_at <= ?) ORDER BY created_at ASC LIMIT 1").get(platform, now) ?? null;
171
- }
172
- return this.db.prepare("SELECT id, user_id, platform, chat_id, description FROM tasks WHERE status = 'auto' AND (scheduled_at IS NULL OR scheduled_at <= ?) ORDER BY created_at ASC LIMIT 1").get(now) ?? null;
173
- }
174
203
  markTaskRunning(taskId) {
175
204
  this.db.prepare("UPDATE tasks SET status = 'running' WHERE id = ?").run(taskId);
176
205
  }
@@ -231,4 +260,53 @@ export class Store {
231
260
  getRecentAutoTasks(platform, limit) {
232
261
  return this.db.prepare("SELECT id, user_id, description, status, parent_id, scheduled_at, created_at FROM tasks WHERE platform = ? AND status IN ('auto','running','done','failed','approval_pending','cancelled') ORDER BY created_at DESC LIMIT ?").all(platform, limit);
233
262
  }
263
+ // --- sub_sessions ---
264
+ createSubSession(id, userId, platform, chatId, label) {
265
+ const now = Date.now();
266
+ this.db.prepare("INSERT INTO sub_sessions (id, user_id, platform, chat_id, claude_session_id, label, status, created_at, last_active_at, message_count, total_cost) VALUES (?, ?, ?, ?, NULL, ?, 'active', ?, ?, 0, 0)").run(id, userId, platform, chatId, label, now, now);
267
+ }
268
+ getSubSession(id) {
269
+ return this.db.prepare("SELECT * FROM sub_sessions WHERE id = ?").get(id) ?? null;
270
+ }
271
+ getActiveSubSessions(userId, platform) {
272
+ return this.db.prepare("SELECT * FROM sub_sessions WHERE user_id = ? AND platform = ? AND status IN ('active','idle') ORDER BY last_active_at DESC").all(userId, platform);
273
+ }
274
+ touchSubSession(id) {
275
+ this.db.prepare("UPDATE sub_sessions SET last_active_at = ?, message_count = message_count + 1, status = 'active' WHERE id = ?").run(Date.now(), id);
276
+ }
277
+ setSubSessionClaudeId(id, claudeSessionId) {
278
+ this.db.prepare("UPDATE sub_sessions SET claude_session_id = ? WHERE id = ?").run(claudeSessionId, id);
279
+ }
280
+ updateSubSessionLabel(id, label) {
281
+ this.db.prepare("UPDATE sub_sessions SET label = ? WHERE id = ?").run(label, id);
282
+ }
283
+ updateSubSessionCost(id, cost) {
284
+ this.db.prepare("UPDATE sub_sessions SET total_cost = total_cost + ? WHERE id = ?").run(cost, id);
285
+ }
286
+ closeSubSession(id) {
287
+ this.db.prepare("UPDATE sub_sessions SET status = 'closed' WHERE id = ?").run(id);
288
+ }
289
+ closeAllSubSessions(userId) {
290
+ this.db.prepare("UPDATE sub_sessions SET status = 'closed' WHERE user_id = ? AND status IN ('active','idle')").run(userId);
291
+ }
292
+ expireIdleSessions(timeoutMs) {
293
+ const cutoff = Date.now() - timeoutMs;
294
+ const r = this.db.prepare("UPDATE sub_sessions SET status = 'expired' WHERE status = 'active' AND last_active_at < ?").run(cutoff);
295
+ return r.changes;
296
+ }
297
+ trackSubSessionMessage(platformMsgId, chatId, subSessionId) {
298
+ this.db.prepare("INSERT OR REPLACE INTO sub_session_messages (platform_msg_id, chat_id, sub_session_id, created_at) VALUES (?, ?, ?, ?)").run(platformMsgId, chatId, subSessionId, Date.now());
299
+ }
300
+ getSubSessionByMessage(platformMsgId, chatId) {
301
+ const row = this.db.prepare("SELECT sub_session_id FROM sub_session_messages WHERE platform_msg_id = ? AND chat_id = ?").get(platformMsgId, chatId);
302
+ return row?.sub_session_id ?? null;
303
+ }
304
+ pruneSubSessionMessages(maxAgeMs) {
305
+ const cutoff = Date.now() - maxAgeMs;
306
+ const r = this.db.prepare("DELETE FROM sub_session_messages WHERE created_at < ?").run(cutoff);
307
+ return r.changes;
308
+ }
309
+ getAllSubSessions(userId) {
310
+ return this.db.prepare("SELECT * FROM sub_sessions WHERE user_id = ? ORDER BY last_active_at DESC").all(userId);
311
+ }
234
312
  }
package/dist/ctl.js CHANGED
@@ -166,7 +166,19 @@ else if (category === "auto") {
166
166
  fail("Usage: auto <add|add-approval|result|list|cancel|clear> ...");
167
167
  }
168
168
  }
169
+ else if (category === "session") {
170
+ if (action === "list") {
171
+ const [userId] = rest;
172
+ if (!userId)
173
+ fail("Usage: session list <user_id>");
174
+ const rows = db.prepare("SELECT id, user_id, platform, chat_id, claude_session_id, label, status, created_at, last_active_at, message_count, total_cost FROM sub_sessions WHERE user_id = ? ORDER BY last_active_at DESC").all(userId);
175
+ output({ ok: true, sessions: rows });
176
+ }
177
+ else {
178
+ fail("Usage: session <list> ...");
179
+ }
180
+ }
169
181
  else {
170
- fail("Usage: claudebridge-ctl <memory|task|reminder|auto> <action> [args...]");
182
+ fail("Usage: claudebridge-ctl <memory|task|reminder|auto|session> <action> [args...]");
171
183
  }
172
184
  db.close();