@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,506 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
|
|
5
|
+
import type { IProviderSessions } from '@/shared/interfaces.js';
|
|
6
|
+
import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
|
7
|
+
import {
|
|
8
|
+
createNormalizedMessage,
|
|
9
|
+
generateMessageId,
|
|
10
|
+
getOpenCodeDatabasePath,
|
|
11
|
+
normalizeProviderTimestamp,
|
|
12
|
+
readObjectRecord,
|
|
13
|
+
readJsonRecord,
|
|
14
|
+
readOptionalString,
|
|
15
|
+
} from '@/shared/utils.js';
|
|
16
|
+
|
|
17
|
+
const PROVIDER = 'opencode';
|
|
18
|
+
|
|
19
|
+
type OpenCodeHistoryRow = {
|
|
20
|
+
message_id: string;
|
|
21
|
+
message_time_created: number | null;
|
|
22
|
+
message_data: string | null;
|
|
23
|
+
part_id: string | null;
|
|
24
|
+
part_time_created: number | null;
|
|
25
|
+
part_data: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type OpenCodeTokenTotals = {
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
reasoningTokens: number;
|
|
32
|
+
cacheReadTokens: number;
|
|
33
|
+
cacheWriteTokens: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const openOpenCodeDatabase = (): Database.Database | null => {
|
|
37
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
38
|
+
if (!fsSync.existsSync(dbPath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatToolContent = (value: unknown): string => {
|
|
46
|
+
if (value === undefined || value === null) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(value, null, 2);
|
|
56
|
+
} catch {
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* OpenCode can persist the first prompt as a JSON string literal inside a text
|
|
63
|
+
* part, for example `"hello"` instead of `hello`. Decode only complete JSON
|
|
64
|
+
* string literals so normal assistant/user prose remains untouched.
|
|
65
|
+
*/
|
|
66
|
+
const unwrapJsonStringLiteral = (value: string): string => {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(trimmed);
|
|
74
|
+
return typeof parsed === 'string' ? parsed : value;
|
|
75
|
+
} catch {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const extractText = (value: unknown): string => {
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
return unwrapJsonStringLiteral(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const record = readObjectRecord(value);
|
|
86
|
+
const text = readOptionalString(record?.text)
|
|
87
|
+
?? readOptionalString(record?.content)
|
|
88
|
+
?? '';
|
|
89
|
+
return unwrapJsonStringLiteral(text);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const hasUserRole = (value: unknown): boolean => {
|
|
93
|
+
const record = readObjectRecord(value);
|
|
94
|
+
return readOptionalString(record?.role) === 'user';
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isUserTextEcho = (raw: AnyRecord): boolean => {
|
|
98
|
+
return readOptionalString(raw.role) === 'user'
|
|
99
|
+
|| hasUserRole(raw.message)
|
|
100
|
+
|| hasUserRole(raw.part);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const buildTokenUsage = (totals: OpenCodeTokenTotals | undefined): AnyRecord | undefined => {
|
|
104
|
+
if (!totals) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const inputTokens = totals.inputTokens;
|
|
109
|
+
const displayInputTokens = inputTokens + totals.cacheReadTokens;
|
|
110
|
+
const outputTokens = totals.outputTokens;
|
|
111
|
+
const used = inputTokens
|
|
112
|
+
+ outputTokens
|
|
113
|
+
+ totals.reasoningTokens
|
|
114
|
+
+ totals.cacheReadTokens
|
|
115
|
+
+ totals.cacheWriteTokens;
|
|
116
|
+
|
|
117
|
+
if (used <= 0) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
used,
|
|
123
|
+
inputTokens: displayInputTokens,
|
|
124
|
+
outputTokens,
|
|
125
|
+
breakdown: {
|
|
126
|
+
input: displayInputTokens,
|
|
127
|
+
output: outputTokens,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const readOpenCodeSessionColumnTokenUsage = (
|
|
133
|
+
db: Database.Database,
|
|
134
|
+
sessionId: string,
|
|
135
|
+
): AnyRecord | undefined => {
|
|
136
|
+
const columns = db.prepare('PRAGMA table_info(session)').all() as { name: string }[];
|
|
137
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
138
|
+
const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
|
|
139
|
+
if (!requiredColumns.every((column) => columnNames.has(column))) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const row = db.prepare(`
|
|
144
|
+
SELECT
|
|
145
|
+
tokens_input AS inputTokens,
|
|
146
|
+
tokens_output AS outputTokens,
|
|
147
|
+
tokens_reasoning AS reasoningTokens,
|
|
148
|
+
tokens_cache_read AS cacheReadTokens,
|
|
149
|
+
tokens_cache_write AS cacheWriteTokens
|
|
150
|
+
FROM session
|
|
151
|
+
WHERE id = ?
|
|
152
|
+
`).get(sessionId) as OpenCodeTokenTotals | undefined;
|
|
153
|
+
|
|
154
|
+
if (!row) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return buildTokenUsage({
|
|
159
|
+
inputTokens: Number(row.inputTokens ?? 0),
|
|
160
|
+
outputTokens: Number(row.outputTokens ?? 0),
|
|
161
|
+
reasoningTokens: Number(row.reasoningTokens ?? 0),
|
|
162
|
+
cacheReadTokens: Number(row.cacheReadTokens ?? 0),
|
|
163
|
+
cacheWriteTokens: Number(row.cacheWriteTokens ?? 0),
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* OpenCode stores per-message token counts on assistant `message.data` objects
|
|
169
|
+
* (see MessageV2.Assistant). Older DBs also had session-level counters; this
|
|
170
|
+
* matches current `opencode.db` layouts that only persist message JSON.
|
|
171
|
+
*/
|
|
172
|
+
const aggregateOpenCodeSessionTokenUsage = (
|
|
173
|
+
db: Database.Database,
|
|
174
|
+
sessionId: string,
|
|
175
|
+
): AnyRecord | undefined => {
|
|
176
|
+
const sessionColumnUsage = readOpenCodeSessionColumnTokenUsage(db, sessionId);
|
|
177
|
+
if (sessionColumnUsage) {
|
|
178
|
+
return sessionColumnUsage;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rows = db.prepare('SELECT data FROM message WHERE session_id = ?').all(sessionId) as { data: string }[];
|
|
182
|
+
|
|
183
|
+
let inputTokens = 0;
|
|
184
|
+
let outputTokens = 0;
|
|
185
|
+
let reasoningTokens = 0;
|
|
186
|
+
let cacheReadTokens = 0;
|
|
187
|
+
let cacheWriteTokens = 0;
|
|
188
|
+
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
const info = readJsonRecord(row.data);
|
|
191
|
+
if (readOptionalString(info?.role) !== 'assistant') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const tokens = readObjectRecord(info?.tokens);
|
|
196
|
+
if (!tokens) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
inputTokens += Number(tokens.input ?? 0);
|
|
201
|
+
outputTokens += Number(tokens.output ?? 0);
|
|
202
|
+
reasoningTokens += Number(tokens.reasoning ?? 0);
|
|
203
|
+
const cache = readObjectRecord(tokens.cache);
|
|
204
|
+
cacheReadTokens += Number(cache?.read ?? 0);
|
|
205
|
+
cacheWriteTokens += Number(cache?.write ?? 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return buildTokenUsage({
|
|
209
|
+
inputTokens,
|
|
210
|
+
outputTokens,
|
|
211
|
+
reasoningTokens,
|
|
212
|
+
cacheReadTokens,
|
|
213
|
+
cacheWriteTokens,
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export class OpenCodeSessionsProvider implements IProviderSessions {
|
|
218
|
+
/**
|
|
219
|
+
* Normalizes live `opencode run --format json` events into frontend messages.
|
|
220
|
+
*/
|
|
221
|
+
normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
|
222
|
+
const raw = readObjectRecord(rawMessage);
|
|
223
|
+
if (!raw) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const type = readOptionalString(raw.type) ?? readOptionalString(raw.event);
|
|
228
|
+
const eventSessionId = readOptionalString(raw.sessionID) ?? readOptionalString(raw.sessionId) ?? sessionId;
|
|
229
|
+
const timestamp = normalizeProviderTimestamp(raw.time ?? raw.timestamp);
|
|
230
|
+
const baseId = readOptionalString(raw.id)
|
|
231
|
+
?? readOptionalString(raw.messageID)
|
|
232
|
+
?? generateMessageId('opencode');
|
|
233
|
+
|
|
234
|
+
if (type === 'text') {
|
|
235
|
+
// The client already renders an optimistic user bubble, so provider user
|
|
236
|
+
// echoes must not be streamed back as assistant text.
|
|
237
|
+
if (isUserTextEcho(raw)) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const content = extractText(raw.text ?? raw.delta ?? raw.message);
|
|
242
|
+
if (!content.trim()) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return [createNormalizedMessage({
|
|
247
|
+
id: baseId,
|
|
248
|
+
sessionId: eventSessionId,
|
|
249
|
+
timestamp,
|
|
250
|
+
provider: PROVIDER,
|
|
251
|
+
kind: 'stream_delta',
|
|
252
|
+
content,
|
|
253
|
+
})];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (type === 'reasoning') {
|
|
257
|
+
const content = extractText(raw.text ?? raw.delta ?? raw.message);
|
|
258
|
+
if (!content.trim()) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [createNormalizedMessage({
|
|
263
|
+
id: baseId,
|
|
264
|
+
sessionId: eventSessionId,
|
|
265
|
+
timestamp,
|
|
266
|
+
provider: PROVIDER,
|
|
267
|
+
kind: 'thinking',
|
|
268
|
+
content,
|
|
269
|
+
})];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (type === 'tool_use') {
|
|
273
|
+
const toolName = readOptionalString(raw.tool) ?? readOptionalString(raw.name) ?? 'Tool';
|
|
274
|
+
const toolId = readOptionalString(raw.callID) ?? readOptionalString(raw.toolCallId) ?? baseId;
|
|
275
|
+
const toolMessage = createNormalizedMessage({
|
|
276
|
+
id: baseId,
|
|
277
|
+
sessionId: eventSessionId,
|
|
278
|
+
timestamp,
|
|
279
|
+
provider: PROVIDER,
|
|
280
|
+
kind: 'tool_use',
|
|
281
|
+
toolName,
|
|
282
|
+
toolInput: raw.input ?? raw.arguments ?? {},
|
|
283
|
+
toolId,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (raw.output !== undefined || raw.error !== undefined) {
|
|
287
|
+
toolMessage.toolResult = {
|
|
288
|
+
content: formatToolContent(raw.output ?? raw.error),
|
|
289
|
+
isError: raw.error !== undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return [toolMessage];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (type === 'error') {
|
|
297
|
+
return [createNormalizedMessage({
|
|
298
|
+
id: baseId,
|
|
299
|
+
sessionId: eventSessionId,
|
|
300
|
+
timestamp,
|
|
301
|
+
provider: PROVIDER,
|
|
302
|
+
kind: 'error',
|
|
303
|
+
content: readOptionalString(raw.error) ?? readOptionalString(raw.message) ?? 'Unknown OpenCode error',
|
|
304
|
+
})];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (type === 'step_finish') {
|
|
308
|
+
return [createNormalizedMessage({
|
|
309
|
+
id: baseId,
|
|
310
|
+
sessionId: eventSessionId,
|
|
311
|
+
timestamp,
|
|
312
|
+
provider: PROVIDER,
|
|
313
|
+
kind: 'stream_end',
|
|
314
|
+
})];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Loads OpenCode history from the shared SQLite session database.
|
|
322
|
+
*/
|
|
323
|
+
async fetchHistory(
|
|
324
|
+
sessionId: string,
|
|
325
|
+
options: FetchHistoryOptions = {},
|
|
326
|
+
): Promise<FetchHistoryResult> {
|
|
327
|
+
const { limit = null, offset = 0 } = options;
|
|
328
|
+
const db = openOpenCodeDatabase();
|
|
329
|
+
if (!db) {
|
|
330
|
+
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const rows = db.prepare(`
|
|
335
|
+
SELECT
|
|
336
|
+
m.id AS message_id,
|
|
337
|
+
m.time_created AS message_time_created,
|
|
338
|
+
m.data AS message_data,
|
|
339
|
+
p.id AS part_id,
|
|
340
|
+
p.time_created AS part_time_created,
|
|
341
|
+
p.data AS part_data
|
|
342
|
+
FROM message m
|
|
343
|
+
LEFT JOIN part p
|
|
344
|
+
ON p.session_id = m.session_id
|
|
345
|
+
AND p.message_id = m.id
|
|
346
|
+
WHERE m.session_id = ?
|
|
347
|
+
ORDER BY
|
|
348
|
+
COALESCE(m.time_created, 0),
|
|
349
|
+
m.id,
|
|
350
|
+
COALESCE(p.time_created, 0),
|
|
351
|
+
p.id
|
|
352
|
+
`).all(sessionId) as OpenCodeHistoryRow[];
|
|
353
|
+
|
|
354
|
+
const normalized = this.normalizeHistoryRows(rows, sessionId);
|
|
355
|
+
const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, sessionId);
|
|
356
|
+
|
|
357
|
+
const normalizedOffset = Math.max(0, offset);
|
|
358
|
+
const normalizedLimit = limit === null ? null : Math.max(0, limit);
|
|
359
|
+
const total = normalized.length;
|
|
360
|
+
const messages = normalizedLimit === null
|
|
361
|
+
? normalized
|
|
362
|
+
: normalized.slice(
|
|
363
|
+
Math.max(0, total - normalizedOffset - normalizedLimit),
|
|
364
|
+
Math.max(0, total - normalizedOffset),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
messages,
|
|
369
|
+
total,
|
|
370
|
+
hasMore: normalizedLimit === null
|
|
371
|
+
? false
|
|
372
|
+
: Math.max(0, total - normalizedOffset - normalizedLimit) > 0,
|
|
373
|
+
offset: normalizedOffset,
|
|
374
|
+
limit: normalizedLimit,
|
|
375
|
+
tokenUsage,
|
|
376
|
+
};
|
|
377
|
+
} catch (error) {
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
console.warn(`[OpenCodeProvider] Failed to load session ${sessionId}:`, message);
|
|
380
|
+
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
381
|
+
} finally {
|
|
382
|
+
db.close();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private normalizeHistoryRows(rows: OpenCodeHistoryRow[], sessionId: string): NormalizedMessage[] {
|
|
387
|
+
const normalized: NormalizedMessage[] = [];
|
|
388
|
+
const emittedMessageErrors = new Set<string>();
|
|
389
|
+
|
|
390
|
+
for (const row of rows) {
|
|
391
|
+
const timestamp = normalizeProviderTimestamp(row.part_time_created ?? row.message_time_created);
|
|
392
|
+
const baseId = `${row.message_id}_${row.part_id ?? normalized.length}`;
|
|
393
|
+
const messageInfo = readJsonRecord(row.message_data);
|
|
394
|
+
const messageRole = readOptionalString(messageInfo?.role);
|
|
395
|
+
|
|
396
|
+
if (
|
|
397
|
+
messageInfo
|
|
398
|
+
&& messageRole === 'assistant'
|
|
399
|
+
&& messageInfo.error != null
|
|
400
|
+
&& !emittedMessageErrors.has(row.message_id)
|
|
401
|
+
) {
|
|
402
|
+
emittedMessageErrors.add(row.message_id);
|
|
403
|
+
normalized.push(createNormalizedMessage({
|
|
404
|
+
id: `${baseId}_error`,
|
|
405
|
+
sessionId,
|
|
406
|
+
timestamp,
|
|
407
|
+
provider: PROVIDER,
|
|
408
|
+
kind: 'error',
|
|
409
|
+
content: formatToolContent(messageInfo.error),
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!row.part_id) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const partData = readJsonRecord(row.part_data) ?? {};
|
|
418
|
+
const partType = readOptionalString(partData.type);
|
|
419
|
+
if (!partType) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (partType === 'text') {
|
|
424
|
+
const content = extractText(partData);
|
|
425
|
+
if (content.trim()) {
|
|
426
|
+
normalized.push(createNormalizedMessage({
|
|
427
|
+
id: baseId,
|
|
428
|
+
sessionId,
|
|
429
|
+
timestamp,
|
|
430
|
+
provider: PROVIDER,
|
|
431
|
+
kind: 'text',
|
|
432
|
+
role: messageRole === 'user' ? 'user' : 'assistant',
|
|
433
|
+
content,
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (partType === 'reasoning') {
|
|
440
|
+
const content = extractText(partData);
|
|
441
|
+
if (content.trim()) {
|
|
442
|
+
normalized.push(createNormalizedMessage({
|
|
443
|
+
id: baseId,
|
|
444
|
+
sessionId,
|
|
445
|
+
timestamp,
|
|
446
|
+
provider: PROVIDER,
|
|
447
|
+
kind: 'thinking',
|
|
448
|
+
content,
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (partType === 'tool') {
|
|
455
|
+
const state = readObjectRecord(partData.state) ?? {};
|
|
456
|
+
const status = readOptionalString(state.status);
|
|
457
|
+
const toolMessage = createNormalizedMessage({
|
|
458
|
+
id: baseId,
|
|
459
|
+
sessionId,
|
|
460
|
+
timestamp,
|
|
461
|
+
provider: PROVIDER,
|
|
462
|
+
kind: 'tool_use',
|
|
463
|
+
toolName: readOptionalString(partData.tool) ?? 'Tool',
|
|
464
|
+
toolInput: state.input ?? partData.input ?? {},
|
|
465
|
+
toolId: readOptionalString(partData.callID) ?? row.part_id,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if (status === 'completed' || status === 'error') {
|
|
469
|
+
toolMessage.toolResult = {
|
|
470
|
+
content: formatToolContent(state.output ?? state.error),
|
|
471
|
+
isError: status === 'error',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
normalized.push(toolMessage);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (partType === 'step-finish') {
|
|
480
|
+
normalized.push(createNormalizedMessage({
|
|
481
|
+
id: baseId,
|
|
482
|
+
sessionId,
|
|
483
|
+
timestamp,
|
|
484
|
+
provider: PROVIDER,
|
|
485
|
+
kind: 'stream_end',
|
|
486
|
+
}));
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (partType === 'patch' || partType === 'agent') {
|
|
491
|
+
normalized.push(createNormalizedMessage({
|
|
492
|
+
id: baseId,
|
|
493
|
+
sessionId,
|
|
494
|
+
timestamp,
|
|
495
|
+
provider: PROVIDER,
|
|
496
|
+
kind: 'tool_use',
|
|
497
|
+
toolName: partType === 'patch' ? 'Patch' : 'Agent',
|
|
498
|
+
toolInput: partData,
|
|
499
|
+
toolId: row.part_id,
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return normalized;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
|
|
5
|
+
import type { ProviderSkillSource } from '@/shared/types.js';
|
|
6
|
+
import {
|
|
7
|
+
addUniqueProviderSkillSource,
|
|
8
|
+
findTopmostGitRoot,
|
|
9
|
+
} from '@/shared/utils.js';
|
|
10
|
+
|
|
11
|
+
const OPENCODE_PROJECT_SKILL_DIRS = [
|
|
12
|
+
['.opencode', 'skills'],
|
|
13
|
+
['.claude', 'skills'],
|
|
14
|
+
['.agents', 'skills'],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const OPENCODE_USER_SKILL_DIRS = [
|
|
18
|
+
['.config', 'opencode', 'skills'],
|
|
19
|
+
['.claude', 'skills'],
|
|
20
|
+
['.agents', 'skills'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export class OpenCodeSkillsProvider extends SkillsProvider {
|
|
24
|
+
constructor() {
|
|
25
|
+
super('opencode');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected async getSkillSources(workspacePath: string): Promise<ProviderSkillSource[]> {
|
|
29
|
+
const sources: ProviderSkillSource[] = [];
|
|
30
|
+
const seenRootDirs = new Set<string>();
|
|
31
|
+
const repoRoot = await findTopmostGitRoot(workspacePath);
|
|
32
|
+
|
|
33
|
+
for (const projectRoot of this.getProjectSearchRoots(workspacePath, repoRoot)) {
|
|
34
|
+
for (const skillDir of OPENCODE_PROJECT_SKILL_DIRS) {
|
|
35
|
+
// OpenCode intentionally reads Claude and Agents skill folders so users
|
|
36
|
+
// can reuse the same skill libraries across compatible coding agents.
|
|
37
|
+
addUniqueProviderSkillSource(sources, seenRootDirs, {
|
|
38
|
+
scope: 'project',
|
|
39
|
+
rootDir: path.join(projectRoot, ...skillDir),
|
|
40
|
+
commandPrefix: '/',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const skillDir of OPENCODE_USER_SKILL_DIRS) {
|
|
46
|
+
addUniqueProviderSkillSource(sources, seenRootDirs, {
|
|
47
|
+
scope: 'user',
|
|
48
|
+
rootDir: path.join(os.homedir(), ...skillDir),
|
|
49
|
+
commandPrefix: '/',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sources;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getProjectSearchRoots(workspacePath: string, repoRoot: string | null): string[] {
|
|
57
|
+
const roots: string[] = [];
|
|
58
|
+
const normalizedWorkspacePath = path.resolve(workspacePath);
|
|
59
|
+
const normalizedRepoRoot = repoRoot ? path.resolve(repoRoot) : null;
|
|
60
|
+
let currentPath = normalizedWorkspacePath;
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
roots.push(currentPath);
|
|
64
|
+
if (!normalizedRepoRoot || currentPath === normalizedRepoRoot) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parentPath = path.dirname(currentPath);
|
|
69
|
+
if (parentPath === currentPath) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
currentPath = parentPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return roots;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { OpenCodeProviderAuth } from '@/modules/providers/list/opencode/opencode-auth.provider.js';
|
|
2
|
+
import { OpenCodeProviderModels } from '@/modules/providers/list/opencode/opencode-models.provider.js';
|
|
3
|
+
import { OpenCodeMcpProvider } from '@/modules/providers/list/opencode/opencode-mcp.provider.js';
|
|
4
|
+
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
|
5
|
+
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
|
6
|
+
import { OpenCodeSkillsProvider } from '@/modules/providers/list/opencode/opencode-skills.provider.js';
|
|
7
|
+
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
|
8
|
+
import type {
|
|
9
|
+
IProviderAuth,
|
|
10
|
+
IProviderModels,
|
|
11
|
+
IProviderSessionSynchronizer,
|
|
12
|
+
IProviderSkills,
|
|
13
|
+
IProviderSessions,
|
|
14
|
+
} from '@/shared/interfaces.js';
|
|
15
|
+
|
|
16
|
+
export class OpenCodeProvider extends AbstractProvider {
|
|
17
|
+
readonly models: IProviderModels = new OpenCodeProviderModels();
|
|
18
|
+
readonly mcp = new OpenCodeMcpProvider();
|
|
19
|
+
readonly auth: IProviderAuth = new OpenCodeProviderAuth();
|
|
20
|
+
readonly skills: IProviderSkills = new OpenCodeSkillsProvider();
|
|
21
|
+
readonly sessions: IProviderSessions = new OpenCodeSessionsProvider();
|
|
22
|
+
readonly sessionSynchronizer: IProviderSessionSynchronizer = new OpenCodeSessionSynchronizer();
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super('opencode');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.
|
|
|
2
2
|
import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js';
|
|
3
3
|
import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js';
|
|
4
4
|
import { GeminiProvider } from '@/modules/providers/list/gemini/gemini.provider.js';
|
|
5
|
+
import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js';
|
|
5
6
|
import type { IProvider } from '@/shared/interfaces.js';
|
|
6
7
|
import type { LLMProvider } from '@/shared/types.js';
|
|
7
8
|
import { AppError } from '@/shared/utils.js';
|
|
@@ -11,6 +12,7 @@ const providers: Record<LLMProvider, IProvider> = {
|
|
|
11
12
|
codex: new CodexProvider(),
|
|
12
13
|
cursor: new CursorProvider(),
|
|
13
14
|
gemini: new GeminiProvider(),
|
|
15
|
+
opencode: new OpenCodeProvider(),
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/**
|