@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.
- package/config.yaml.example +12 -3
- package/dist/adapters/discord.d.ts +3 -0
- package/dist/adapters/discord.js +92 -20
- package/dist/adapters/telegram.d.ts +3 -0
- package/dist/adapters/telegram.js +124 -59
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +246 -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 +5 -22
- package/dist/core/lock.d.ts +8 -4
- package/dist/core/lock.js +25 -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 +95 -17
- package/dist/ctl.js +13 -1
- package/dist/index.js +30 -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 {
|
|
@@ -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
|
-
|
|
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();
|