@emqo/claudebridge 0.7.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.
- package/config.yaml.example +12 -3
- package/dist/adapters/base.d.ts +1 -0
- package/dist/adapters/discord.d.ts +4 -0
- package/dist/adapters/discord.js +99 -23
- package/dist/adapters/telegram.d.ts +4 -0
- package/dist/adapters/telegram.js +138 -60
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +251 -232
- package/dist/core/config.d.ts +2 -70
- package/dist/core/config.js +9 -38
- package/dist/core/i18n.js +12 -4
- package/dist/core/keys.d.ts +2 -10
- package/dist/core/keys.js +7 -22
- package/dist/core/lock.d.ts +8 -4
- package/dist/core/lock.js +28 -17
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +24 -0
- package/dist/core/router.d.ts +25 -0
- package/dist/core/router.js +125 -0
- package/dist/core/schema.d.ts +166 -0
- package/dist/core/schema.js +85 -0
- package/dist/core/session.d.ts +50 -0
- package/dist/core/session.js +100 -0
- package/dist/core/store.d.ts +52 -15
- package/dist/core/store.js +105 -19
- package/dist/ctl.js +32 -30
- package/dist/index.js +42 -13
- package/dist/providers/base.d.ts +26 -0
- package/dist/providers/base.js +1 -0
- package/dist/providers/claude.d.ts +9 -0
- package/dist/providers/claude.js +53 -0
- package/dist/providers/codex.d.ts +9 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/registry.d.ts +2 -0
- package/dist/providers/registry.js +12 -0
- package/dist/skills/bridge.d.ts +2 -0
- package/dist/skills/bridge.js +2 -0
- package/dist/webhook.js +7 -5
- package/package.json +8 -4
|
@@ -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
|
+
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/store.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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 {
|
|
@@ -70,11 +95,37 @@ export class Store {
|
|
|
70
95
|
catch { }
|
|
71
96
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
|
|
72
97
|
// Startup recovery: reset orphaned 'running' tasks back to 'auto' so they get re-executed
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
const orphaned = this.db.prepare("SELECT id, description FROM tasks WHERE status = 'running'").all();
|
|
99
|
+
for (const t of orphaned) {
|
|
100
|
+
const desc = t.description.startsWith("[recovered]") ? t.description : `[recovered] Check current state before making changes — previous attempt was interrupted. Original task: ${t.description}`;
|
|
101
|
+
this.db.prepare("UPDATE tasks SET status = 'auto', description = ? WHERE id = ?").run(desc, t.id);
|
|
102
|
+
}
|
|
103
|
+
if (orphaned.length > 0) {
|
|
104
|
+
log.info("recovered orphaned running tasks", { count: orphaned.length });
|
|
105
|
+
}
|
|
106
|
+
// Startup cleanup: prune history/usage older than 30 days
|
|
107
|
+
const cutoff = Date.now() - 30 * 86400000;
|
|
108
|
+
this.db.prepare("DELETE FROM history WHERE created_at < ?").run(cutoff);
|
|
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 });
|
|
76
124
|
}
|
|
77
125
|
}
|
|
126
|
+
close() {
|
|
127
|
+
this.db.close();
|
|
128
|
+
}
|
|
78
129
|
// --- sessions ---
|
|
79
130
|
getSession(userId) {
|
|
80
131
|
const row = this.db
|
|
@@ -143,26 +194,12 @@ export class Store {
|
|
|
143
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());
|
|
144
195
|
return Number(r.lastInsertRowid);
|
|
145
196
|
}
|
|
146
|
-
getTasks(userId) {
|
|
147
|
-
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);
|
|
148
|
-
}
|
|
149
|
-
completeTask(taskId, userId) {
|
|
150
|
-
const r = this.db.prepare("UPDATE tasks SET status = 'done' WHERE id = ? AND user_id = ? AND status = 'pending'").run(taskId, userId);
|
|
151
|
-
return r.changes > 0;
|
|
152
|
-
}
|
|
153
197
|
getDueReminders() {
|
|
154
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());
|
|
155
199
|
}
|
|
156
200
|
markReminderSent(taskId) {
|
|
157
201
|
this.db.prepare("UPDATE tasks SET reminder_sent = 1 WHERE id = ?").run(taskId);
|
|
158
202
|
}
|
|
159
|
-
getNextAutoTask(platform) {
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
if (platform) {
|
|
162
|
-
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;
|
|
163
|
-
}
|
|
164
|
-
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;
|
|
165
|
-
}
|
|
166
203
|
markTaskRunning(taskId) {
|
|
167
204
|
this.db.prepare("UPDATE tasks SET status = 'running' WHERE id = ?").run(taskId);
|
|
168
205
|
}
|
|
@@ -223,4 +260,53 @@ export class Store {
|
|
|
223
260
|
getRecentAutoTasks(platform, limit) {
|
|
224
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);
|
|
225
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
|
+
}
|
|
226
312
|
}
|