@4via6/relay 1.0.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/.env.example +50 -0
- package/README.md +253 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/bot.d.ts +2 -0
- package/dist/bot.js +14 -0
- package/dist/bot.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/admin.d.ts +2 -0
- package/dist/commands/admin.js +420 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +80 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.js +162 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +152 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +21 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +105 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/media.d.ts +2 -0
- package/dist/commands/media.js +248 -0
- package/dist/commands/media.js.map +1 -0
- package/dist/commands/monitor.d.ts +2 -0
- package/dist/commands/monitor.js +136 -0
- package/dist/commands/monitor.js.map +1 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +114 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/shell.d.ts +2 -0
- package/dist/commands/shell.js +90 -0
- package/dist/commands/shell.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/claude.d.ts +52 -0
- package/dist/providers/claude.js +449 -0
- package/dist/providers/claude.js.map +1 -0
- package/dist/providers/codex.d.ts +45 -0
- package/dist/providers/codex.js +400 -0
- package/dist/providers/codex.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +40 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/opencode.d.ts +40 -0
- package/dist/providers/opencode.js +498 -0
- package/dist/providers/opencode.js.map +1 -0
- package/dist/providers/types.d.ts +173 -0
- package/dist/providers/types.js +6 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/session.d.ts +10 -0
- package/dist/session.js +65 -0
- package/dist/session.js.map +1 -0
- package/dist/utils/chunker.d.ts +1 -0
- package/dist/utils/chunker.js +24 -0
- package/dist/utils/chunker.js.map +1 -0
- package/dist/utils/errors.d.ts +12 -0
- package/dist/utils/errors.js +85 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/files.d.ts +15 -0
- package/dist/utils/files.js +81 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/html.d.ts +4 -0
- package/dist/utils/html.js +10 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/media.d.ts +15 -0
- package/dist/utils/media.js +103 -0
- package/dist/utils/media.js.map +1 -0
- package/dist/utils/store.d.ts +15 -0
- package/dist/utils/store.js +44 -0
- package/dist/utils/store.js.map +1 -0
- package/dist/utils/stream.d.ts +21 -0
- package/dist/utils/stream.js +149 -0
- package/dist/utils/stream.js.map +1 -0
- package/dist/utils/stt.d.ts +7 -0
- package/dist/utils/stt.js +118 -0
- package/dist/utils/stt.js.map +1 -0
- package/dist/utils/system-prompt.d.ts +5 -0
- package/dist/utils/system-prompt.js +64 -0
- package/dist/utils/system-prompt.js.map +1 -0
- package/dist/utils/timeout.d.ts +2 -0
- package/dist/utils/timeout.js +18 -0
- package/dist/utils/timeout.js.map +1 -0
- package/docs/commands.md +351 -0
- package/docs/configuration.md +160 -0
- package/docs/features.md +443 -0
- package/docs/getting-started.md +102 -0
- package/docs/providers.md +236 -0
- package/docs/troubleshooting.md +287 -0
- package/package.json +54 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider abstraction layer for Relay.
|
|
3
|
+
* Each coding agent platform (OpenCode, Claude, Codex) implements this interface.
|
|
4
|
+
*/
|
|
5
|
+
export interface Session {
|
|
6
|
+
id: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
lastModified?: number;
|
|
13
|
+
}
|
|
14
|
+
export type MessagePart = {
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "file";
|
|
19
|
+
mime: string;
|
|
20
|
+
filename?: string;
|
|
21
|
+
url: string;
|
|
22
|
+
};
|
|
23
|
+
export interface PromptOptions {
|
|
24
|
+
model?: {
|
|
25
|
+
providerID: string;
|
|
26
|
+
modelID: string;
|
|
27
|
+
};
|
|
28
|
+
system?: string;
|
|
29
|
+
parts?: MessagePart[];
|
|
30
|
+
}
|
|
31
|
+
export interface PromptResult {
|
|
32
|
+
text: string;
|
|
33
|
+
parts?: unknown[];
|
|
34
|
+
raw?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface StreamChunk {
|
|
37
|
+
type: "text" | "tool_use" | "status" | "done" | "file";
|
|
38
|
+
content: string;
|
|
39
|
+
file?: {
|
|
40
|
+
mime: string;
|
|
41
|
+
filename: string;
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface Todo {
|
|
46
|
+
content: string;
|
|
47
|
+
status: string;
|
|
48
|
+
priority?: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface FileDiff {
|
|
52
|
+
file: string;
|
|
53
|
+
additions: number;
|
|
54
|
+
deletions: number;
|
|
55
|
+
before?: string;
|
|
56
|
+
after?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SearchResult {
|
|
59
|
+
file: string;
|
|
60
|
+
line?: number;
|
|
61
|
+
text?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface FileStatus {
|
|
64
|
+
path: string;
|
|
65
|
+
status: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ProjectInfo {
|
|
68
|
+
id?: string;
|
|
69
|
+
worktree?: string;
|
|
70
|
+
directory?: string;
|
|
71
|
+
vcs?: string;
|
|
72
|
+
branch?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface CommandInfo {
|
|
75
|
+
name: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ModelInfo {
|
|
79
|
+
id: string;
|
|
80
|
+
name?: string;
|
|
81
|
+
provider?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface ModelDetail {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
provider: string;
|
|
87
|
+
reasoning: boolean;
|
|
88
|
+
attachment: boolean;
|
|
89
|
+
modalities?: {
|
|
90
|
+
input: string[];
|
|
91
|
+
output: string[];
|
|
92
|
+
};
|
|
93
|
+
active: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface HealthInfo {
|
|
96
|
+
status: string;
|
|
97
|
+
provider: string;
|
|
98
|
+
model?: string;
|
|
99
|
+
project?: string;
|
|
100
|
+
branch?: string;
|
|
101
|
+
extra?: Record<string, string>;
|
|
102
|
+
}
|
|
103
|
+
export interface McpServerConfig {
|
|
104
|
+
type: "local" | "remote";
|
|
105
|
+
command?: string[];
|
|
106
|
+
environment?: Record<string, string>;
|
|
107
|
+
url?: string;
|
|
108
|
+
headers?: Record<string, string>;
|
|
109
|
+
enabled?: boolean;
|
|
110
|
+
timeout?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface McpServerStatus {
|
|
113
|
+
name: string;
|
|
114
|
+
status: "connected" | "disabled" | "failed" | "needs_auth" | "unknown";
|
|
115
|
+
error?: string;
|
|
116
|
+
}
|
|
117
|
+
export interface ProviderCapabilities {
|
|
118
|
+
streaming: boolean;
|
|
119
|
+
todos: boolean;
|
|
120
|
+
diff: boolean;
|
|
121
|
+
fork: boolean;
|
|
122
|
+
revert: boolean;
|
|
123
|
+
share: boolean;
|
|
124
|
+
summarize: boolean;
|
|
125
|
+
history: boolean;
|
|
126
|
+
fileOps: boolean;
|
|
127
|
+
shell: boolean;
|
|
128
|
+
commands: boolean;
|
|
129
|
+
fileOutput: boolean;
|
|
130
|
+
mcp: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface Provider {
|
|
133
|
+
/** Provider identifier */
|
|
134
|
+
readonly name: "opencode" | "claude" | "codex";
|
|
135
|
+
/** Declared capabilities for this provider */
|
|
136
|
+
readonly capabilities: ProviderCapabilities;
|
|
137
|
+
init(): Promise<void>;
|
|
138
|
+
shutdown(): void;
|
|
139
|
+
createSession(title?: string): Promise<Session>;
|
|
140
|
+
listSessions(): Promise<SessionInfo[]>;
|
|
141
|
+
getSession(id: string): Promise<Session | null>;
|
|
142
|
+
deleteSession(id: string): Promise<boolean>;
|
|
143
|
+
prompt(sessionId: string, text: string, options?: PromptOptions): Promise<PromptResult>;
|
|
144
|
+
abort(sessionId: string): Promise<void>;
|
|
145
|
+
promptStream?(sessionId: string, text: string, options?: PromptOptions): AsyncGenerator<StreamChunk>;
|
|
146
|
+
getTodos(sessionId: string): Promise<Todo[] | null>;
|
|
147
|
+
getDiff(sessionId: string): Promise<FileDiff[] | null>;
|
|
148
|
+
forkSession(sessionId: string, messageId?: string): Promise<Session | null>;
|
|
149
|
+
revert(sessionId: string): Promise<boolean>;
|
|
150
|
+
unrevert(sessionId: string): Promise<boolean>;
|
|
151
|
+
share(sessionId: string): Promise<string | null>;
|
|
152
|
+
summarize(sessionId: string): Promise<boolean>;
|
|
153
|
+
getHistory(sessionId: string, limit?: number): Promise<unknown[] | null>;
|
|
154
|
+
readFile(path: string): Promise<string | null>;
|
|
155
|
+
findFiles(query: string): Promise<string[] | null>;
|
|
156
|
+
searchText(pattern: string): Promise<SearchResult[] | null>;
|
|
157
|
+
findSymbols(query: string): Promise<unknown[] | null>;
|
|
158
|
+
getFileStatus(): Promise<FileStatus[] | null>;
|
|
159
|
+
shell(sessionId: string, command: string): Promise<string | null>;
|
|
160
|
+
runCommand(sessionId: string, command: string, args?: string): Promise<PromptResult | null>;
|
|
161
|
+
getProjectInfo(): Promise<ProjectInfo | null>;
|
|
162
|
+
getTools(): Promise<string[] | null>;
|
|
163
|
+
getCommands(): Promise<CommandInfo[] | null>;
|
|
164
|
+
getHealth(): Promise<HealthInfo>;
|
|
165
|
+
getConfig(): Promise<unknown>;
|
|
166
|
+
getProviders(): Promise<unknown>;
|
|
167
|
+
getAgents(): Promise<unknown[] | null>;
|
|
168
|
+
listModels(): Promise<ModelDetail[]>;
|
|
169
|
+
getMcpStatus(): Promise<McpServerStatus[] | null>;
|
|
170
|
+
addMcpServer(name: string, config: McpServerConfig): Promise<boolean>;
|
|
171
|
+
removeMcpServer(name: string): Promise<boolean>;
|
|
172
|
+
}
|
|
173
|
+
export type ProviderName = Provider["name"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getOrCreateSession(): Promise<string>;
|
|
2
|
+
export declare function getActiveSessionId(): string | null;
|
|
3
|
+
export declare function setActiveSessionId(id: string): void;
|
|
4
|
+
export declare function clearActiveSession(): void;
|
|
5
|
+
export declare function getSelectedModel(): {
|
|
6
|
+
providerID: string;
|
|
7
|
+
modelID: string;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare function setSelectedModel(providerID: string, modelID: string): void;
|
|
10
|
+
export declare function clearSelectedModel(): void;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getProvider } from "./providers/index.js";
|
|
2
|
+
import { JsonStore } from "./utils/store.js";
|
|
3
|
+
const store = new JsonStore("session.json", {
|
|
4
|
+
activeSessionId: null,
|
|
5
|
+
selectedModel: null,
|
|
6
|
+
});
|
|
7
|
+
// Load persisted state (or fall back to env var for model)
|
|
8
|
+
const persisted = store.load();
|
|
9
|
+
let activeSessionId = persisted.activeSessionId;
|
|
10
|
+
let selectedModel = persisted.selectedModel ?? (() => {
|
|
11
|
+
const envModel = process.env.OPENCODE_MODEL;
|
|
12
|
+
if (envModel && envModel.includes("/")) {
|
|
13
|
+
const [providerID, ...rest] = envModel.split("/");
|
|
14
|
+
return { providerID, modelID: rest.join("/") };
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
})();
|
|
18
|
+
// Mutex to prevent double-create race when concurrent messages arrive
|
|
19
|
+
let createSessionPromise = null;
|
|
20
|
+
export async function getOrCreateSession() {
|
|
21
|
+
if (activeSessionId)
|
|
22
|
+
return activeSessionId;
|
|
23
|
+
// If another call is already creating a session, wait for it
|
|
24
|
+
if (createSessionPromise)
|
|
25
|
+
return createSessionPromise;
|
|
26
|
+
createSessionPromise = (async () => {
|
|
27
|
+
try {
|
|
28
|
+
const provider = getProvider();
|
|
29
|
+
const session = await provider.createSession("Telegram Session");
|
|
30
|
+
activeSessionId = session.id;
|
|
31
|
+
persist();
|
|
32
|
+
return activeSessionId;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
createSessionPromise = null;
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return createSessionPromise;
|
|
39
|
+
}
|
|
40
|
+
export function getActiveSessionId() {
|
|
41
|
+
return activeSessionId;
|
|
42
|
+
}
|
|
43
|
+
export function setActiveSessionId(id) {
|
|
44
|
+
activeSessionId = id;
|
|
45
|
+
persist();
|
|
46
|
+
}
|
|
47
|
+
export function clearActiveSession() {
|
|
48
|
+
activeSessionId = null;
|
|
49
|
+
persist();
|
|
50
|
+
}
|
|
51
|
+
export function getSelectedModel() {
|
|
52
|
+
return selectedModel;
|
|
53
|
+
}
|
|
54
|
+
export function setSelectedModel(providerID, modelID) {
|
|
55
|
+
selectedModel = { providerID, modelID };
|
|
56
|
+
persist();
|
|
57
|
+
}
|
|
58
|
+
export function clearSelectedModel() {
|
|
59
|
+
selectedModel = null;
|
|
60
|
+
persist();
|
|
61
|
+
}
|
|
62
|
+
function persist() {
|
|
63
|
+
store.save({ activeSessionId, selectedModel });
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAO7C,MAAM,KAAK,GAAG,IAAI,SAAS,CAAe,cAAc,EAAE;IACxD,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,IAAI;CACpB,CAAC,CAAC;AAEH,2DAA2D;AAC3D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;AAE/B,IAAI,eAAe,GAAkB,SAAS,CAAC,eAAe,CAAC;AAC/D,IAAI,aAAa,GAAmD,SAAS,CAAC,aAAa,IAAI,CAAC,GAAG,EAAE;IACnG,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC5C,IAAI,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC,EAAE,CAAC;AAEL,sEAAsE;AACtE,IAAI,oBAAoB,GAA2B,IAAI,CAAC;AAExD,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,eAAe;QAAE,OAAO,eAAe,CAAC;IAE5C,6DAA6D;IAC7D,IAAI,oBAAoB;QAAE,OAAO,oBAAoB,CAAC;IAEtD,oBAAoB,GAAG,CAAC,KAAK,IAAI,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;YACjE,eAAe,GAAG,OAAO,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,CAAC;YACV,OAAO,eAAe,CAAC;QACzB,CAAC;gBAAS,CAAC;YACT,oBAAoB,GAAG,IAAI,CAAC;QAC9B,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,eAAe,GAAG,EAAE,CAAC;IACrB,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,eAAe,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,OAAe;IAClE,aAAa,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACxC,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,aAAa,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,OAAO;IACd,KAAK,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function chunkMessage(text: string, maxLen?: number): string[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MAX_LENGTH = 4096;
|
|
2
|
+
export function chunkMessage(text, maxLen = MAX_LENGTH) {
|
|
3
|
+
if (text.length <= maxLen)
|
|
4
|
+
return [text];
|
|
5
|
+
const chunks = [];
|
|
6
|
+
let remaining = text;
|
|
7
|
+
while (remaining.length > 0) {
|
|
8
|
+
if (remaining.length <= maxLen) {
|
|
9
|
+
chunks.push(remaining);
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
let splitAt = remaining.lastIndexOf("\n\n", maxLen);
|
|
13
|
+
if (splitAt < maxLen * 0.3)
|
|
14
|
+
splitAt = remaining.lastIndexOf("\n", maxLen);
|
|
15
|
+
if (splitAt < maxLen * 0.2)
|
|
16
|
+
splitAt = remaining.lastIndexOf(" ", maxLen);
|
|
17
|
+
if (splitAt < 1)
|
|
18
|
+
splitAt = maxLen;
|
|
19
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
20
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
21
|
+
}
|
|
22
|
+
return chunks;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../src/utils/chunker.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,SAAiB,UAAU;IACpE,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,SAAS,GAAG,IAAI,CAAC;IAErB,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,IAAI,SAAS,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACvB,MAAM;QACR,CAAC;QAED,IAAI,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACpD,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG;YAAE,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1E,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG;YAAE,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzE,IAAI,OAAO,GAAG,CAAC;YAAE,OAAO,GAAG,MAAM,CAAC;QAElC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACzC,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error formatting for user-facing Telegram messages.
|
|
3
|
+
* All output is HTML-formatted for parse_mode: "HTML".
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Format a caught exception into a user-friendly HTML message with context.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatCatchError(err: unknown, context: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Message for empty AI responses (no text returned).
|
|
11
|
+
*/
|
|
12
|
+
export declare const EMPTY_RESPONSE_MSG: string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error formatting for user-facing Telegram messages.
|
|
3
|
+
* All output is HTML-formatted for parse_mode: "HTML".
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from "./html.js";
|
|
6
|
+
const MAX_ERROR_LENGTH = 200;
|
|
7
|
+
/**
|
|
8
|
+
* Format a caught exception into a user-friendly HTML message with context.
|
|
9
|
+
*/
|
|
10
|
+
export function formatCatchError(err, context) {
|
|
11
|
+
const msg = extractMessage(err);
|
|
12
|
+
// Connection refused / server unreachable
|
|
13
|
+
if (matchesAny(msg, ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "fetch failed", "network", "socket"])) {
|
|
14
|
+
return (`<b>Server unreachable</b>\n\n` +
|
|
15
|
+
`Cannot reach the AI server. Make sure the coding agent is running and accessible.`);
|
|
16
|
+
}
|
|
17
|
+
// Timeout
|
|
18
|
+
if (matchesAny(msg, ["timeout", "timed out", "AbortError", "aborted"])) {
|
|
19
|
+
return (`<b>Request timed out</b>\n\n` +
|
|
20
|
+
`The server took too long to respond. The model may be overloaded — try again in a moment.`);
|
|
21
|
+
}
|
|
22
|
+
// Rate limit from exceptions
|
|
23
|
+
if (matchesAny(msg, ["rate limit", "too many requests", "429", "413", "tokens per minute"])) {
|
|
24
|
+
return (`<b>Rate limit exceeded</b>\n\n` +
|
|
25
|
+
`The AI provider is throttling requests. Wait a moment and try again.\n\n` +
|
|
26
|
+
`<i>${escapeHtml(truncate(msg))}</i>`);
|
|
27
|
+
}
|
|
28
|
+
// Model not found from exceptions
|
|
29
|
+
if (matchesAny(msg, ["model not found", "ProviderModelNotFoundError"])) {
|
|
30
|
+
return (`<b>Model not found</b>\n\n` +
|
|
31
|
+
`The selected model isn't available. Use /model to check or /providers to see what's available.\n\n` +
|
|
32
|
+
`<i>${escapeHtml(truncate(msg))}</i>`);
|
|
33
|
+
}
|
|
34
|
+
// Generic — show what happened and why
|
|
35
|
+
return (`<b>Error ${escapeHtml(context)}</b>\n\n` +
|
|
36
|
+
`<i>${escapeHtml(truncate(msg))}</i>`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Message for empty AI responses (no text returned).
|
|
40
|
+
*/
|
|
41
|
+
export const EMPTY_RESPONSE_MSG = `<b>Empty response</b>\n\n` +
|
|
42
|
+
`The AI returned no text. This can happen when:\n` +
|
|
43
|
+
`• The model's token limit was exceeded\n` +
|
|
44
|
+
`• The provider is overloaded or rate-limiting\n` +
|
|
45
|
+
`• The request was too large for the free tier\n\n` +
|
|
46
|
+
`Try again, or switch to a different model with /model.`;
|
|
47
|
+
// --- Helpers ---
|
|
48
|
+
function extractMessage(error) {
|
|
49
|
+
if (typeof error === "string")
|
|
50
|
+
return error;
|
|
51
|
+
if (error && typeof error === "object") {
|
|
52
|
+
const e = error;
|
|
53
|
+
// SDK error shapes: { message }, { error: { message } }, { error: string }
|
|
54
|
+
if (typeof e.message === "string" && e.message)
|
|
55
|
+
return e.message;
|
|
56
|
+
if (e.error && typeof e.error === "object" && typeof e.error.message === "string")
|
|
57
|
+
return e.error.message;
|
|
58
|
+
if (typeof e.error === "string")
|
|
59
|
+
return e.error;
|
|
60
|
+
// Status code errors — don't include full body (may contain secrets)
|
|
61
|
+
if (e.statusCode || e.status) {
|
|
62
|
+
const code = e.statusCode ?? e.status;
|
|
63
|
+
return `HTTP ${code}`;
|
|
64
|
+
}
|
|
65
|
+
// Last resort — stringify but keep it short
|
|
66
|
+
try {
|
|
67
|
+
const json = JSON.stringify(error);
|
|
68
|
+
return json.length > 300 ? json.slice(0, 300) + "..." : json;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return String(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return "Unknown error";
|
|
75
|
+
}
|
|
76
|
+
function matchesAny(text, patterns) {
|
|
77
|
+
const lower = text.toLowerCase();
|
|
78
|
+
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
79
|
+
}
|
|
80
|
+
function truncate(text) {
|
|
81
|
+
if (text.length <= MAX_ERROR_LENGTH)
|
|
82
|
+
return text;
|
|
83
|
+
return text.slice(0, MAX_ERROR_LENGTH) + "...";
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Context } from "grammy";
|
|
2
|
+
export interface ResponseFile {
|
|
3
|
+
mime: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract file parts from a provider response's parts array.
|
|
9
|
+
* Handles both top-level file parts and tool attachment file parts.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractFileParts(parts: unknown[]): ResponseFile[];
|
|
12
|
+
/**
|
|
13
|
+
* Send extracted files to the Telegram chat as photos or documents.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendResponseFiles(ctx: Context, files: ResponseFile[]): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { InputFile } from "grammy";
|
|
2
|
+
/**
|
|
3
|
+
* Extract file parts from a provider response's parts array.
|
|
4
|
+
* Handles both top-level file parts and tool attachment file parts.
|
|
5
|
+
*/
|
|
6
|
+
export function extractFileParts(parts) {
|
|
7
|
+
const files = [];
|
|
8
|
+
if (!Array.isArray(parts))
|
|
9
|
+
return files;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (!part || typeof part !== "object")
|
|
12
|
+
continue;
|
|
13
|
+
const p = part;
|
|
14
|
+
if (p.type === "file" && p.url) {
|
|
15
|
+
files.push({
|
|
16
|
+
mime: p.mime ?? "application/octet-stream",
|
|
17
|
+
filename: p.filename ?? "file",
|
|
18
|
+
url: p.url,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// Tool parts may have attachments (e.g. OpenCode ToolStateCompleted)
|
|
22
|
+
if (p.type === "tool" && p.state?.attachments) {
|
|
23
|
+
for (const att of p.state.attachments) {
|
|
24
|
+
if (att?.type === "file" && att.url) {
|
|
25
|
+
files.push({
|
|
26
|
+
mime: att.mime ?? "application/octet-stream",
|
|
27
|
+
filename: att.filename ?? "file",
|
|
28
|
+
url: att.url,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Send extracted files to the Telegram chat as photos or documents.
|
|
38
|
+
*/
|
|
39
|
+
export async function sendResponseFiles(ctx, files) {
|
|
40
|
+
const chatId = ctx.chat?.id;
|
|
41
|
+
if (!chatId)
|
|
42
|
+
return;
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
try {
|
|
45
|
+
const buffer = await resolveFileBuffer(file.url);
|
|
46
|
+
const input = new InputFile(buffer, file.filename);
|
|
47
|
+
if (file.mime.startsWith("image/")) {
|
|
48
|
+
await ctx.api.sendPhoto(chatId, input, {
|
|
49
|
+
caption: file.filename,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await ctx.api.sendDocument(chatId, input, {
|
|
54
|
+
caption: file.filename,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// Log but don't fail the whole response for one bad file
|
|
60
|
+
console.error(`Failed to send file ${file.filename}:`, err?.message ?? err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function resolveFileBuffer(url) {
|
|
65
|
+
if (url.startsWith("data:")) {
|
|
66
|
+
// data:[<mediatype>][;base64],<data>
|
|
67
|
+
const commaIdx = url.indexOf(",");
|
|
68
|
+
if (commaIdx === -1)
|
|
69
|
+
throw new Error("Invalid data URL");
|
|
70
|
+
const data = url.slice(commaIdx + 1);
|
|
71
|
+
const isBase64 = url.slice(0, commaIdx).includes(";base64");
|
|
72
|
+
return Buffer.from(data, isBase64 ? "base64" : "utf-8");
|
|
73
|
+
}
|
|
74
|
+
// HTTP(S) URL
|
|
75
|
+
const response = await fetch(url);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
return Buffer.from(await response.arrayBuffer());
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/utils/files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/utils/html.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function downloadTelegramFile(botToken: string, filePath: string, fileName: string): Promise<string>;
|
|
2
|
+
export declare function downloadTelegramFileBuffer(botToken: string, filePath: string): Promise<Buffer>;
|
|
3
|
+
export declare function getUploadDir(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Remove uploaded files older than maxAgeMs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cleanupUploads(maxAgeMs?: number): void;
|
|
8
|
+
/**
|
|
9
|
+
* Start periodic upload cleanup (call once at startup).
|
|
10
|
+
*/
|
|
11
|
+
export declare function startUploadCleanup(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Stop the periodic upload cleanup (call on shutdown).
|
|
14
|
+
*/
|
|
15
|
+
export declare function stopUploadCleanup(): void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { basename, join, resolve } from "path";
|
|
3
|
+
const UPLOAD_DIR = join(process.cwd(), "uploads");
|
|
4
|
+
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
|
|
5
|
+
const CLEANUP_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
6
|
+
const CLEANUP_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
|
7
|
+
function ensureUploadDir() {
|
|
8
|
+
if (!existsSync(UPLOAD_DIR)) {
|
|
9
|
+
mkdirSync(UPLOAD_DIR, { recursive: true, mode: 0o700 });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize a file name to prevent path traversal.
|
|
14
|
+
* Strips directory components and removes unsafe characters.
|
|
15
|
+
*/
|
|
16
|
+
function sanitizeFileName(name) {
|
|
17
|
+
let safe = basename(name);
|
|
18
|
+
safe = safe.replace(/[\x00-\x1f]/g, "");
|
|
19
|
+
if (!safe || safe === "." || safe === "..") {
|
|
20
|
+
safe = `file_${Date.now()}`;
|
|
21
|
+
}
|
|
22
|
+
return safe;
|
|
23
|
+
}
|
|
24
|
+
export async function downloadTelegramFile(botToken, filePath, fileName) {
|
|
25
|
+
ensureUploadDir();
|
|
26
|
+
const safeName = sanitizeFileName(fileName);
|
|
27
|
+
const localPath = join(UPLOAD_DIR, safeName);
|
|
28
|
+
// Verify resolved path stays under UPLOAD_DIR
|
|
29
|
+
if (!resolve(localPath).startsWith(resolve(UPLOAD_DIR))) {
|
|
30
|
+
throw new Error("Invalid file name");
|
|
31
|
+
}
|
|
32
|
+
const url = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
|
|
33
|
+
const response = await fetch(url);
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Failed to download file (HTTP ${response.status})`);
|
|
36
|
+
}
|
|
37
|
+
const contentLength = Number(response.headers.get("content-length") || 0);
|
|
38
|
+
if (contentLength > MAX_FILE_SIZE) {
|
|
39
|
+
throw new Error(`File too large (${Math.round(contentLength / 1024 / 1024)}MB). Maximum is 20MB.`);
|
|
40
|
+
}
|
|
41
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
42
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
43
|
+
throw new Error(`File too large (${Math.round(buffer.length / 1024 / 1024)}MB). Maximum is 20MB.`);
|
|
44
|
+
}
|
|
45
|
+
writeFileSync(localPath, buffer, { mode: 0o600 });
|
|
46
|
+
return localPath;
|
|
47
|
+
}
|
|
48
|
+
export async function downloadTelegramFileBuffer(botToken, filePath) {
|
|
49
|
+
const url = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
|
|
50
|
+
const response = await fetch(url);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`Failed to download file (HTTP ${response.status})`);
|
|
53
|
+
}
|
|
54
|
+
const contentLength = Number(response.headers.get("content-length") || 0);
|
|
55
|
+
if (contentLength > MAX_FILE_SIZE) {
|
|
56
|
+
throw new Error(`File too large (${Math.round(contentLength / 1024 / 1024)}MB). Maximum is 20MB.`);
|
|
57
|
+
}
|
|
58
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
59
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
60
|
+
throw new Error(`File too large (${Math.round(buffer.length / 1024 / 1024)}MB). Maximum is 20MB.`);
|
|
61
|
+
}
|
|
62
|
+
return buffer;
|
|
63
|
+
}
|
|
64
|
+
export function getUploadDir() {
|
|
65
|
+
return UPLOAD_DIR;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Remove uploaded files older than maxAgeMs.
|
|
69
|
+
*/
|
|
70
|
+
export function cleanupUploads(maxAgeMs = CLEANUP_MAX_AGE_MS) {
|
|
71
|
+
if (!existsSync(UPLOAD_DIR))
|
|
72
|
+
return;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const file of readdirSync(UPLOAD_DIR)) {
|
|
75
|
+
const fp = join(UPLOAD_DIR, file);
|
|
76
|
+
try {
|
|
77
|
+
const stat = statSync(fp);
|
|
78
|
+
if (now - stat.mtimeMs > maxAgeMs)
|
|
79
|
+
unlinkSync(fp);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// File may have been deleted between readdir and stat
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let cleanupInterval = null;
|
|
87
|
+
/**
|
|
88
|
+
* Start periodic upload cleanup (call once at startup).
|
|
89
|
+
*/
|
|
90
|
+
export function startUploadCleanup() {
|
|
91
|
+
cleanupUploads();
|
|
92
|
+
cleanupInterval = setInterval(() => cleanupUploads(), CLEANUP_INTERVAL_MS);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stop the periodic upload cleanup (call on shutdown).
|
|
96
|
+
*/
|
|
97
|
+
export function stopUploadCleanup() {
|
|
98
|
+
if (cleanupInterval) {
|
|
99
|
+
clearInterval(cleanupInterval);
|
|
100
|
+
cleanupInterval = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/utils/media.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC;AAClD,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAChD,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AACpD,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,aAAa;AAEzD,SAAS,eAAe;IACtB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC3C,IAAI,GAAG,QAAQ,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAAgB,EAChB,QAAgB,EAChB,QAAgB;IAEhB,eAAe,EAAE,CAAC;IAElB,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAE7C,8CAA8C;IAC9C,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,GAAG,GAAG,oCAAoC,QAAQ,IAAI,QAAQ,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1E,IAAI,aAAa,GAAG,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,GAAG,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACrG,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACrG,CAAC;IAED,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,QAAgB,EAChB,QAAgB;IAEhB,MAAM,GAAG,GAAG,oCAAoC,QAAQ,IAAI,QAAQ,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1E,IAAI,aAAa,GAAG,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,GAAG,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACrG,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACrG,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,QAAQ,GAAG,kBAAkB;IAC1D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC1B,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,QAAQ;gBAAE,UAAU,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;QACxD,CAAC;IACH,CAAC;AACH,CAAC;AAED,IAAI,eAAe,GAA0C,IAAI,CAAC;AAElE;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,cAAc,EAAE,CAAC;IACjB,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,EAAE,mBAAmB,CAAC,CAAC;AAC7E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,eAAe,EAAE,CAAC;QACpB,aAAa,CAAC,eAAe,CAAC,CAAC;QAC/B,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC;AACH,CAAC"}
|