@glwhappen/web-code 1.32.7 → 1.32.10
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/README.de.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/api-docs.html +6 -7
- package/dist/assets/{index-CCGk0QgG.js → index-BLLsK3sG.js} +277 -262
- package/dist/assets/index-Dl5QP21C.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +57 -34
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -3
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +3 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +34 -0
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +131 -19
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +42 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +264 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js +6 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +32 -4
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +287 -0
- package/dist-server/server/opencode-cli.js.map +1 -0
- package/dist-server/server/opencode-cli.test.js +84 -0
- package/dist-server/server/opencode-cli.test.js.map +1 -0
- package/dist-server/server/routes/agent.js +21 -8
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +202 -209
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +2 -2
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/settings.js +0 -10
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/tests/commands.test.js +76 -0
- package/dist-server/server/routes/tests/commands.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +286 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +3 -1
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +841 -0
- package/server/claude-sdk.js +64 -35
- package/server/cursor-cli.js +6 -3
- package/server/gemini-cli.js +7 -1
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +150 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
- package/server/modules/providers/README.md +11 -3
- package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +158 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
- package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +62 -2
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +325 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
- package/server/modules/providers/tests/mcp.test.ts +93 -6
- package/server/modules/providers/tests/opencode-models.test.ts +73 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +336 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +66 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +7 -0
- package/server/openai-codex.js +40 -4
- package/server/opencode-cli.js +336 -0
- package/server/opencode-cli.test.js +95 -0
- package/server/routes/agent.js +22 -8
- package/server/routes/commands.js +254 -233
- package/server/routes/cursor.js +2 -2
- package/server/routes/settings.js +1 -10
- package/server/routes/tests/commands.test.js +82 -0
- package/server/shared/interfaces.ts +45 -0
- package/server/shared/types.ts +88 -1
- package/server/shared/utils.ts +384 -0
- package/dist/assets/index-DdxLnCfK.css +0 -32
- package/dist-server/shared/modelConstants.js +0 -99
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -107
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import crossSpawn from 'cross-spawn';
|
|
5
|
+
|
|
6
|
+
import type { IProviderModels } from '@/shared/interfaces.js';
|
|
7
|
+
import type {
|
|
8
|
+
ProviderChangeActiveModelInput,
|
|
9
|
+
ProviderCurrentActiveModel,
|
|
10
|
+
ProviderModelOption,
|
|
11
|
+
ProviderModelsDefinition,
|
|
12
|
+
ProviderSessionActiveModelChange,
|
|
13
|
+
} from '@/shared/types.js';
|
|
14
|
+
import {
|
|
15
|
+
buildDefaultProviderCurrentActiveModel,
|
|
16
|
+
getOpenCodeDatabasePath,
|
|
17
|
+
readObjectRecord,
|
|
18
|
+
readOptionalString,
|
|
19
|
+
writeProviderSessionActiveModelChange,
|
|
20
|
+
} from '@/shared/utils.js';
|
|
21
|
+
|
|
22
|
+
export const OPENCODE_FALLBACK_MODELS: ProviderModelsDefinition = {
|
|
23
|
+
OPTIONS: [
|
|
24
|
+
{
|
|
25
|
+
value: 'anthropic/claude-sonnet-4-5',
|
|
26
|
+
label: 'Claude Sonnet 4.5',
|
|
27
|
+
description: 'anthropic - anthropic/claude-sonnet-4-5',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: 'anthropic/claude-opus-4-1',
|
|
31
|
+
label: 'Claude Opus 4.1',
|
|
32
|
+
description: 'anthropic - anthropic/claude-opus-4-1',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'anthropic/claude-haiku-4-5',
|
|
36
|
+
label: 'Claude Haiku 4.5',
|
|
37
|
+
description: 'anthropic - anthropic/claude-haiku-4-5',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: 'openai/gpt-5.1',
|
|
41
|
+
label: 'GPT-5.1',
|
|
42
|
+
description: 'openai - openai/gpt-5.1',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: 'openai/gpt-5.1-codex',
|
|
46
|
+
label: 'GPT-5.1 Codex',
|
|
47
|
+
description: 'openai - openai/gpt-5.1-codex',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'openai/gpt-5.4-mini',
|
|
51
|
+
label: 'GPT-5.4 Mini',
|
|
52
|
+
description: 'openai - openai/gpt-5.4-mini',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: 'google/gemini-2.5-pro',
|
|
56
|
+
label: 'Gemini 2.5 Pro',
|
|
57
|
+
description: 'google - google/gemini-2.5-pro',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'google/gemini-2.5-flash',
|
|
61
|
+
label: 'Gemini 2.5 Flash',
|
|
62
|
+
description: 'google - google/gemini-2.5-flash',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
DEFAULT: 'anthropic/claude-sonnet-4-5',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const OPEN_CODE_MODELS_TIMEOUT_MS = 20_000;
|
|
69
|
+
const MODEL_ID_LINE = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i;
|
|
70
|
+
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
71
|
+
const DATE_TOKEN = /^\d{8}$/;
|
|
72
|
+
const SIMPLE_NUMBER_TOKEN = /^\d$/;
|
|
73
|
+
const VERSION_TOKEN = /^[a-z]\d+$/i;
|
|
74
|
+
const NUMERIC_TOKEN = /^\d+(?:\.\d+)*$/;
|
|
75
|
+
const SHORT_ACRONYM_TOKEN = /^[a-z]{2,3}$/;
|
|
76
|
+
|
|
77
|
+
export const parseOpenCodeModelsStdout = (stdout: string): string[] => {
|
|
78
|
+
const ids: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line || line.startsWith('{') || line.startsWith('[')) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (MODEL_ID_LINE.test(line)) {
|
|
87
|
+
ids.push(line);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...new Set(ids)];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const formatDateToken = (token: string): string => (
|
|
95
|
+
`${token.slice(0, 4)}-${token.slice(4, 6)}-${token.slice(6, 8)}`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const formatModelToken = (token: string, nextToken?: string): string => {
|
|
99
|
+
const lower = token.toLowerCase();
|
|
100
|
+
|
|
101
|
+
if (VERSION_TOKEN.test(token)) {
|
|
102
|
+
return token.toUpperCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (SHORT_ACRONYM_TOKEN.test(lower) && nextToken && NUMERIC_TOKEN.test(nextToken)) {
|
|
106
|
+
return token.toUpperCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const formatOpenCodeModelSlug = (slug: string): string => {
|
|
113
|
+
const labelParts: string[] = [];
|
|
114
|
+
const dateParts: string[] = [];
|
|
115
|
+
const tokens = slug.split('-').filter(Boolean);
|
|
116
|
+
|
|
117
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
118
|
+
const token = tokens[index];
|
|
119
|
+
const nextToken = tokens[index + 1];
|
|
120
|
+
|
|
121
|
+
if (DATE_TOKEN.test(token)) {
|
|
122
|
+
dateParts.push(formatDateToken(token));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (SIMPLE_NUMBER_TOKEN.test(token) && nextToken && SIMPLE_NUMBER_TOKEN.test(nextToken)) {
|
|
127
|
+
labelParts.push(`${token}.${nextToken}`);
|
|
128
|
+
index += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
labelParts.push(formatModelToken(token, nextToken));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const label = (labelParts.join(' ').trim() || slug).replace(/^GPT\s+/, 'GPT-');
|
|
136
|
+
if (dateParts.length === 0) {
|
|
137
|
+
return label;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return `${label} (${dateParts.join(', ')})`;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const readOpenCodeModelParts = (id: string): { upstreamProvider: string; slug: string } => {
|
|
144
|
+
const separatorIndex = id.indexOf('/');
|
|
145
|
+
if (separatorIndex < 0) {
|
|
146
|
+
return {
|
|
147
|
+
upstreamProvider: '',
|
|
148
|
+
slug: id,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
upstreamProvider: id.slice(0, separatorIndex),
|
|
154
|
+
slug: id.slice(separatorIndex + 1),
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const labelForOpenCodeModelId = (id: string): string => {
|
|
159
|
+
const fallbackLabel = OPENCODE_FALLBACK_MODELS.OPTIONS.find((option) => option.value === id)?.label;
|
|
160
|
+
if (fallbackLabel) {
|
|
161
|
+
return fallbackLabel;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { slug } = readOpenCodeModelParts(id);
|
|
165
|
+
return formatOpenCodeModelSlug(slug);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const descriptionForOpenCodeModelId = (id: string): string => {
|
|
169
|
+
const { upstreamProvider } = readOpenCodeModelParts(id);
|
|
170
|
+
return upstreamProvider ? `${upstreamProvider} - ${id}` : id;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const buildOpenCodeDefinitionFromIds = (ids: string[]): ProviderModelsDefinition => {
|
|
174
|
+
const options: ProviderModelOption[] = ids.map((value) => ({
|
|
175
|
+
value,
|
|
176
|
+
label: labelForOpenCodeModelId(value),
|
|
177
|
+
description: descriptionForOpenCodeModelId(value),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const defaultValue = options.find((option) => option.value === OPENCODE_FALLBACK_MODELS.DEFAULT)?.value
|
|
181
|
+
?? options[0]?.value
|
|
182
|
+
?? OPENCODE_FALLBACK_MODELS.DEFAULT;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
OPTIONS: options,
|
|
186
|
+
DEFAULT: defaultValue,
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const parseOpenCodeSessionModelValue = (rawModel: unknown): string | null => {
|
|
191
|
+
if (typeof rawModel === 'string') {
|
|
192
|
+
const trimmed = rawModel.trim();
|
|
193
|
+
if (!trimmed) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
return parseOpenCodeSessionModelValue(JSON.parse(trimmed));
|
|
199
|
+
} catch {
|
|
200
|
+
return trimmed;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const record = readObjectRecord(rawModel);
|
|
205
|
+
if (!record) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return readOptionalString(record.id)
|
|
210
|
+
?? readOptionalString(record.model)
|
|
211
|
+
?? readOptionalString(record.name)
|
|
212
|
+
?? readOptionalString(record.value)
|
|
213
|
+
?? null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const runOpenCodeModelsCommand = (): Promise<string> => new Promise((resolve, reject) => {
|
|
217
|
+
const openCodeProcess = spawnFunction('opencode', ['models'], {
|
|
218
|
+
cwd: process.cwd(),
|
|
219
|
+
env: { ...process.env },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let stdout = '';
|
|
223
|
+
let stderr = '';
|
|
224
|
+
let settled = false;
|
|
225
|
+
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
openCodeProcess.kill('SIGTERM');
|
|
228
|
+
if (!settled) {
|
|
229
|
+
settled = true;
|
|
230
|
+
reject(new Error('opencode models timed out'));
|
|
231
|
+
}
|
|
232
|
+
}, OPEN_CODE_MODELS_TIMEOUT_MS);
|
|
233
|
+
|
|
234
|
+
const finish = (error: Error | null, output: string) => {
|
|
235
|
+
if (settled) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
settled = true;
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
|
|
242
|
+
if (error) {
|
|
243
|
+
reject(error);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
resolve(output);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
openCodeProcess.stdout?.on('data', (chunk: Buffer) => {
|
|
251
|
+
stdout += chunk.toString();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
openCodeProcess.stderr?.on('data', (chunk: Buffer) => {
|
|
255
|
+
stderr += chunk.toString();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
openCodeProcess.on('error', (error) => {
|
|
259
|
+
finish(error instanceof Error ? error : new Error(String(error)), '');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
openCodeProcess.on('close', (code) => {
|
|
263
|
+
if (code !== 0) {
|
|
264
|
+
finish(new Error(stderr.trim() || `opencode models exited with code ${code}`), '');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
finish(null, stdout);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
export class OpenCodeProviderModels implements IProviderModels {
|
|
273
|
+
async getSupportedModels(): Promise<ProviderModelsDefinition> {
|
|
274
|
+
try {
|
|
275
|
+
const stdout = await runOpenCodeModelsCommand();
|
|
276
|
+
const ids = parseOpenCodeModelsStdout(stdout);
|
|
277
|
+
if (ids.length === 0) {
|
|
278
|
+
return OPENCODE_FALLBACK_MODELS;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return buildOpenCodeDefinitionFromIds(ids);
|
|
282
|
+
} catch {
|
|
283
|
+
return OPENCODE_FALLBACK_MODELS;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
|
|
288
|
+
if (!sessionId?.trim()) {
|
|
289
|
+
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
294
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const row = db.prepare(`
|
|
298
|
+
SELECT
|
|
299
|
+
s.id AS sessionId,
|
|
300
|
+
s.model AS model,
|
|
301
|
+
s.agent AS agent,
|
|
302
|
+
s.directory AS directory,
|
|
303
|
+
s.time_updated AS timeUpdated,
|
|
304
|
+
s.time_created AS timeCreated
|
|
305
|
+
FROM session s
|
|
306
|
+
WHERE s.id = ?
|
|
307
|
+
ORDER BY COALESCE(s.time_updated, s.time_created, 0) DESC
|
|
308
|
+
LIMIT 1
|
|
309
|
+
`).get(sessionId) as {
|
|
310
|
+
sessionId?: string;
|
|
311
|
+
model?: unknown;
|
|
312
|
+
agent?: string | null;
|
|
313
|
+
directory?: string | null;
|
|
314
|
+
timeUpdated?: number | null;
|
|
315
|
+
timeCreated?: number | null;
|
|
316
|
+
} | undefined;
|
|
317
|
+
|
|
318
|
+
const model = parseOpenCodeSessionModelValue(row?.model);
|
|
319
|
+
if (model) {
|
|
320
|
+
return {
|
|
321
|
+
model,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
} finally {
|
|
325
|
+
db.close();
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Fall through to the provider default when OpenCode session lookup fails.
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async changeActiveModel(
|
|
335
|
+
input: ProviderChangeActiveModelInput,
|
|
336
|
+
): Promise<ProviderSessionActiveModelChange> {
|
|
337
|
+
return writeProviderSessionActiveModelChange('opencode', input);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
|
|
6
|
+
import { sessionsDb } from '@/modules/database/index.js';
|
|
7
|
+
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
|
|
8
|
+
import {
|
|
9
|
+
getOpenCodeDatabasePath,
|
|
10
|
+
normalizeProviderTimestamp,
|
|
11
|
+
normalizeSessionName,
|
|
12
|
+
readJsonRecord,
|
|
13
|
+
readOptionalString,
|
|
14
|
+
} from '@/shared/utils.js';
|
|
15
|
+
|
|
16
|
+
type OpenCodeSessionRow = {
|
|
17
|
+
id: string;
|
|
18
|
+
directory: string | null;
|
|
19
|
+
title: string | null;
|
|
20
|
+
time_created: number | null;
|
|
21
|
+
time_updated: number | null;
|
|
22
|
+
worktree: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type SynchronizeRowsResult = {
|
|
26
|
+
processed: number;
|
|
27
|
+
firstSessionId: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Session indexer for OpenCode's SQLite-backed session store.
|
|
32
|
+
*/
|
|
33
|
+
export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
34
|
+
private readonly provider = 'opencode' as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Scans OpenCode's shared opencode.db and upserts active sessions into DB.
|
|
38
|
+
*/
|
|
39
|
+
async synchronize(since: Date | undefined, ownerUserId: number): Promise<number> {
|
|
40
|
+
const result = this.synchronizeRows(ownerUserId, since);
|
|
41
|
+
return result.processed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handles watcher changes for opencode.db.
|
|
46
|
+
*/
|
|
47
|
+
async synchronizeFile(filePath: string, ownerUserId: number): Promise<string | null> {
|
|
48
|
+
if (path.basename(filePath) !== 'opencode.db') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = this.synchronizeRows(ownerUserId, undefined, 1);
|
|
53
|
+
return result.firstSessionId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private synchronizeRows(ownerUserId: number, since?: Date, limit?: number): SynchronizeRowsResult {
|
|
57
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
58
|
+
if (!fsSync.existsSync(dbPath)) {
|
|
59
|
+
return { processed: 0, firstSessionId: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
63
|
+
try {
|
|
64
|
+
const sinceMillis = since?.getTime() ?? null;
|
|
65
|
+
const limitClause = limit ? 'LIMIT ?' : '';
|
|
66
|
+
const params = limit ? [sinceMillis, sinceMillis, limit] : [sinceMillis, sinceMillis];
|
|
67
|
+
const rows = db.prepare(`
|
|
68
|
+
SELECT
|
|
69
|
+
s.id AS id,
|
|
70
|
+
s.directory AS directory,
|
|
71
|
+
s.title AS title,
|
|
72
|
+
s.time_created AS time_created,
|
|
73
|
+
s.time_updated AS time_updated,
|
|
74
|
+
p.worktree AS worktree
|
|
75
|
+
FROM session s
|
|
76
|
+
LEFT JOIN project p ON p.id = s.project_id
|
|
77
|
+
WHERE s.time_archived IS NULL
|
|
78
|
+
AND (? IS NULL OR COALESCE(s.time_updated, s.time_created, 0) >= ?)
|
|
79
|
+
ORDER BY COALESCE(s.time_updated, s.time_created, 0) DESC, s.id DESC
|
|
80
|
+
${limitClause}
|
|
81
|
+
`).all(...params) as OpenCodeSessionRow[];
|
|
82
|
+
|
|
83
|
+
let processed = 0;
|
|
84
|
+
let firstSessionId: string | null = null;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const indexedSessionId = this.upsertSession(ownerUserId, db, row);
|
|
87
|
+
if (!indexedSessionId) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!firstSessionId) {
|
|
92
|
+
firstSessionId = indexedSessionId;
|
|
93
|
+
}
|
|
94
|
+
processed += 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { processed, firstSessionId };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
console.warn('[OpenCodeProvider] Failed to synchronize sessions:', message);
|
|
101
|
+
return { processed: 0, firstSessionId: null };
|
|
102
|
+
} finally {
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private upsertSession(ownerUserId: number, db: Database.Database, row: OpenCodeSessionRow): string | null {
|
|
108
|
+
const sessionId = readOptionalString(row.id);
|
|
109
|
+
const projectPath = readOptionalString(row.directory) ?? readOptionalString(row.worktree);
|
|
110
|
+
if (!sessionId || !projectPath) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fallbackTitle = 'Untitled OpenCode Session';
|
|
115
|
+
const existingSession = sessionsDb.getSessionById(ownerUserId, sessionId);
|
|
116
|
+
const existingName = existingSession?.custom_name;
|
|
117
|
+
const nextName = existingName && existingName !== fallbackTitle
|
|
118
|
+
? existingName
|
|
119
|
+
: readOptionalString(row.title) ?? this.readFirstUserText(db, sessionId);
|
|
120
|
+
|
|
121
|
+
// OpenCode stores every session in one shared sqlite database, so jsonl_path
|
|
122
|
+
// must stay null to avoid deleting opencode.db when one app session is removed.
|
|
123
|
+
sessionsDb.createSession(
|
|
124
|
+
ownerUserId,
|
|
125
|
+
sessionId,
|
|
126
|
+
this.provider,
|
|
127
|
+
projectPath,
|
|
128
|
+
normalizeSessionName(nextName, fallbackTitle),
|
|
129
|
+
normalizeProviderTimestamp(row.time_created),
|
|
130
|
+
normalizeProviderTimestamp(row.time_updated ?? row.time_created),
|
|
131
|
+
null,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return sessionId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private readFirstUserText(db: Database.Database, sessionId: string): string | undefined {
|
|
138
|
+
try {
|
|
139
|
+
const row = db.prepare(`
|
|
140
|
+
SELECT p.data AS data
|
|
141
|
+
FROM message m
|
|
142
|
+
INNER JOIN part p
|
|
143
|
+
ON p.session_id = m.session_id
|
|
144
|
+
AND p.message_id = m.id
|
|
145
|
+
WHERE m.session_id = ?
|
|
146
|
+
AND json_extract(m.data, '$.role') = 'user'
|
|
147
|
+
AND json_extract(p.data, '$.type') = 'text'
|
|
148
|
+
ORDER BY COALESCE(m.time_created, 0), COALESCE(p.time_created, 0)
|
|
149
|
+
LIMIT 1
|
|
150
|
+
`).get(sessionId) as { data: string | null } | undefined;
|
|
151
|
+
|
|
152
|
+
const data = readJsonRecord(row?.data);
|
|
153
|
+
return readOptionalString(data?.text);
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|