@livedesk/hub 0.1.15 → 0.1.16

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.
@@ -1,260 +0,0 @@
1
- import { AgentProviderError, isRetryableProviderError } from './provider-errors.js';
2
- import { DEFAULT_AGENT_BASE_URL } from './agent-settings.js';
3
-
4
- export const OPENCODE_GO_DEFAULT_MODEL_ID = 'deepseek-v4-flash';
5
-
6
- const AGENT_OPERATIONS = new Set([
7
- 'system.health',
8
- 'gpu.status',
9
- 'disk.status',
10
- 'process.list',
11
- 'service.status',
12
- 'diagnostics.collect'
13
- ]);
14
-
15
- function normalizeBaseUrl(value) {
16
- try {
17
- const url = new URL(String(value || DEFAULT_AGENT_BASE_URL));
18
- if (!['http:', 'https:'].includes(url.protocol)) throw new Error('protocol');
19
- return url.toString().replace(/\/+$/, '');
20
- } catch {
21
- throw new AgentProviderError('agent-invalid-base-url', 'The Agent provider URL is invalid.', { status: 400 });
22
- }
23
- }
24
-
25
- function normalizeCapabilities(source = {}) {
26
- const capabilities = source.capabilities && typeof source.capabilities === 'object' ? source.capabilities : source;
27
- return {
28
- supportsReasoningLevel: capabilities.supportsReasoningLevel === true || capabilities.reasoning === true,
29
- reasoningParameter: typeof capabilities.reasoningParameter === 'string' ? capabilities.reasoningParameter : '',
30
- supportsTemperature: capabilities.supportsTemperature !== false,
31
- supportsTopP: capabilities.supportsTopP !== false,
32
- supportsStreaming: capabilities.supportsStreaming !== false,
33
- supportsTools: capabilities.supportsTools === true,
34
- supportsJsonSchema: capabilities.supportsJsonSchema === true
35
- };
36
- }
37
-
38
- function normalizeModel(model) {
39
- const id = String(model?.id || model?.model || '').trim();
40
- if (!id) return null;
41
- const protocolValue = String(model?.protocol || model?.api?.protocol || model?.type || '').trim().toLowerCase();
42
- const protocol = /anthropic|messages/.test(protocolValue) ? 'anthropic-messages' : 'openai-chat-completions';
43
- return {
44
- id,
45
- name: String(model?.name || model?.display_name || id).trim().slice(0, 200),
46
- protocol,
47
- available: model?.available !== false && protocol === 'openai-chat-completions',
48
- capabilities: normalizeCapabilities(model)
49
- };
50
- }
51
-
52
- export function normalizeModelList(payload) {
53
- const source = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : [];
54
- return source.map(normalizeModel).filter(Boolean);
55
- }
56
-
57
- function delay(ms) {
58
- return new Promise(resolve => setTimeout(resolve, ms));
59
- }
60
-
61
- function contentFromCompletion(payload) {
62
- const content = payload?.choices?.[0]?.message?.content ?? payload?.choices?.[0]?.text ?? '';
63
- if (Array.isArray(content)) return content.map(item => typeof item === 'string' ? item : String(item?.text || '')).join('');
64
- return String(content || '');
65
- }
66
-
67
- function parsePlan(content) {
68
- const trimmed = String(content || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
69
- let candidate = trimmed;
70
- try {
71
- return JSON.parse(candidate);
72
- } catch {
73
- const start = trimmed.indexOf('{');
74
- const end = trimmed.lastIndexOf('}');
75
- if (start < 0 || end <= start) throw new AgentProviderError('agent-provider-invalid-response', 'The Agent provider returned an invalid plan.', { status: 502 });
76
- candidate = trimmed.slice(start, end + 1);
77
- try {
78
- return JSON.parse(candidate);
79
- } catch {
80
- throw new AgentProviderError('agent-provider-invalid-response', 'The Agent provider returned an invalid plan.', { status: 502 });
81
- }
82
- }
83
- }
84
-
85
- function validatePlan(value) {
86
- const operation = String(value?.operation || '').trim();
87
- if (!AGENT_OPERATIONS.has(operation)) {
88
- throw new AgentProviderError('agent-provider-invalid-plan', 'The Agent provider returned an unsupported operation.', { status: 502 });
89
- }
90
- const targetQuery = operation === 'process.list' && typeof value?.targetQuery === 'string'
91
- ? value.targetQuery.replace(/[\0\r\n]/g, ' ').trim().slice(0, 120)
92
- : '';
93
- return {
94
- operation,
95
- targetQuery: targetQuery || undefined,
96
- title: typeof value?.title === 'string' ? value.title.replace(/[\0\r\n]/g, ' ').trim().slice(0, 120) || undefined : undefined
97
- };
98
- }
99
-
100
- export function createOpenCodeGoProvider() {
101
- async function requestJson({ settings, apiKey, endpoint, method = 'GET', body, timeoutMs }) {
102
- if (!apiKey) throw new AgentProviderError('agent-api-key-missing', 'OpenCode Go API key is not configured.', { status: 401 });
103
- const url = `${normalizeBaseUrl(settings.baseUrl)}/${String(endpoint).replace(/^\/+/, '')}`;
104
- const retryCount = Math.max(0, Math.min(4, Number(settings.retryCount) || 0));
105
- const retryDelayMs = Math.max(100, Math.min(10000, Number(settings.retryDelayMs) || 1000));
106
- for (let attempt = 0; attempt <= retryCount; attempt += 1) {
107
- const controller = new AbortController();
108
- const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 60000));
109
- try {
110
- const response = await fetch(url, {
111
- method,
112
- headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
113
- body: body === undefined ? undefined : JSON.stringify(body),
114
- signal: controller.signal
115
- });
116
- clearTimeout(timer);
117
- if (response.ok) return await response.json();
118
- const status = response.status;
119
- const retryable = status === 408 || status === 429 || status >= 500;
120
- if (retryable && attempt < retryCount) {
121
- await delay(retryDelayMs * (attempt + 1));
122
- continue;
123
- }
124
- if (status === 401 || status === 403) throw new AgentProviderError('agent-api-key-invalid', 'OpenCode Go rejected the API key.', { status: 401 });
125
- if (status === 429) throw new AgentProviderError('agent-provider-rate-limited', 'OpenCode Go is rate limiting requests.', { status: 429, retryable: true });
126
- throw new AgentProviderError(status >= 500 ? 'agent-provider-upstream' : 'agent-provider-request-invalid', 'OpenCode Go rejected the request.', { status: status >= 500 ? 502 : 400, retryable });
127
- } catch (error) {
128
- clearTimeout(timer);
129
- if (error instanceof AgentProviderError) {
130
- if (isRetryableProviderError(error) && attempt < retryCount) {
131
- await delay(retryDelayMs * (attempt + 1));
132
- continue;
133
- }
134
- throw error;
135
- }
136
- const code = error?.name === 'AbortError' ? 'agent-provider-timeout' : 'agent-provider-network';
137
- const message = code === 'agent-provider-timeout' ? 'OpenCode Go request timed out.' : 'OpenCode Go could not be reached.';
138
- const providerError = new AgentProviderError(code, message, { status: code === 'agent-provider-timeout' ? 504 : 502, retryable: true });
139
- if (attempt < retryCount) {
140
- await delay(retryDelayMs * (attempt + 1));
141
- continue;
142
- }
143
- throw providerError;
144
- }
145
- }
146
- throw new AgentProviderError('agent-provider-network', 'OpenCode Go could not be reached.', { status: 502, retryable: true });
147
- }
148
-
149
- async function getModels(settings, apiKey) {
150
- return normalizeModelList(await requestJson({ settings, apiKey, endpoint: 'models', timeoutMs: settings.summaryTimeoutMs }));
151
- }
152
-
153
- async function selectedModel(settings, apiKey, modelId) {
154
- const models = await getModels(settings, apiKey);
155
- const selected = models.find(model => model.id === modelId);
156
- if (models.length > 0 && (!selected || !selected.available)) {
157
- throw new AgentProviderError('agent-model-unavailable', 'The selected model is not available for LiveDesk.', { status: 409 });
158
- }
159
- return { models, model: selected || { id: modelId, name: modelId, available: true, protocol: 'openai-chat-completions', capabilities: normalizeCapabilities() } };
160
- }
161
-
162
- function completionBody({ model, messages, temperature, topP, maxTokens, reasoningLevel }) {
163
- const body = { model: model.id, messages, max_tokens: maxTokens };
164
- if (model.capabilities.supportsTemperature) body.temperature = temperature;
165
- if (model.capabilities.supportsTopP) body.top_p = topP;
166
- if (reasoningLevel !== 'auto' && model.capabilities.supportsReasoningLevel && model.capabilities.reasoningParameter) {
167
- body[model.capabilities.reasoningParameter] = reasoningLevel;
168
- }
169
- return body;
170
- }
171
-
172
- async function testConnection(settings, apiKey) {
173
- const startedAt = Date.now();
174
- const modelId = settings.defaultModelId || OPENCODE_GO_DEFAULT_MODEL_ID;
175
- const { model } = await selectedModel(settings, apiKey, modelId);
176
- const payload = await requestJson({
177
- settings,
178
- apiKey,
179
- endpoint: 'chat/completions',
180
- method: 'POST',
181
- timeoutMs: settings.planTimeoutMs,
182
- body: completionBody({
183
- model,
184
- messages: [
185
- { role: 'system', content: 'Return only the JSON object {"ok":true}. Do not add markdown.' },
186
- { role: 'user', content: 'Connection test.' }
187
- ],
188
- temperature: 0,
189
- topP: 1,
190
- maxTokens: 32,
191
- reasoningLevel: 'auto'
192
- })
193
- });
194
- if (!contentFromCompletion(payload)) throw new AgentProviderError('agent-provider-invalid-response', 'OpenCode Go returned an empty response.', { status: 502 });
195
- return { ok: true, modelId: model.id, modelName: model.name, latencyMs: Date.now() - startedAt };
196
- }
197
-
198
- async function createPlan({ instruction }, settings, apiKey) {
199
- const normalizedInstruction = String(instruction || '').replace(/[\0]/g, ' ').trim().slice(0, 2000);
200
- if (!normalizedInstruction) throw new AgentProviderError('agent-invalid-instruction', 'Instruction is required.', { status: 400 });
201
- const modelId = settings.planModelId || settings.defaultModelId || OPENCODE_GO_DEFAULT_MODEL_ID;
202
- const { model } = await selectedModel(settings, apiKey, modelId);
203
- const payload = await requestJson({
204
- settings,
205
- apiKey,
206
- endpoint: 'chat/completions',
207
- method: 'POST',
208
- timeoutMs: settings.planTimeoutMs,
209
- body: completionBody({
210
- model,
211
- messages: [
212
- {
213
- role: 'system',
214
- content: 'You are the LiveDesk read-only task planner. Return JSON only with exactly these keys: operation, targetQuery, title. Allowed operation values are system.health, gpu.status, disk.status, process.list, service.status, diagnostics.collect. Never suggest shell commands, PowerShell, Bash, file changes, installs, deletion, reboot, credentials, or arbitrary tools. Use targetQuery only for process.list; otherwise use null. If the request is outside the allowlist, return {"operation":null,"targetQuery":null,"title":"Unsupported request"}.'
215
- },
216
- { role: 'user', content: normalizedInstruction }
217
- ],
218
- temperature: settings.planTemperature,
219
- topP: settings.topP,
220
- maxTokens: settings.planMaxOutputTokens,
221
- reasoningLevel: settings.reasoningLevel
222
- })
223
- });
224
- return validatePlan(parsePlan(contentFromCompletion(payload)));
225
- }
226
-
227
- async function createSummary({ instruction, operation, summary }, settings, apiKey) {
228
- const modelId = settings.summaryModelId || settings.defaultModelId || OPENCODE_GO_DEFAULT_MODEL_ID;
229
- const { model } = await selectedModel(settings, apiKey, modelId);
230
- const payload = await requestJson({
231
- settings,
232
- apiKey,
233
- endpoint: 'chat/completions',
234
- method: 'POST',
235
- timeoutMs: settings.summaryTimeoutMs,
236
- body: completionBody({
237
- model,
238
- messages: [
239
- {
240
- role: 'system',
241
- content: 'You summarize completed LiveDesk read-only checks. Return plain text only, in at most three short sentences. Never include or invent commands, credentials, file paths, or remediation instructions.'
242
- },
243
- {
244
- role: 'user',
245
- content: JSON.stringify({ instruction: String(instruction || '').slice(0, 500), operation, summary })
246
- }
247
- ],
248
- temperature: settings.summaryTemperature,
249
- topP: settings.topP,
250
- maxTokens: settings.summaryMaxOutputTokens,
251
- reasoningLevel: 'auto'
252
- })
253
- });
254
- const text = contentFromCompletion(payload).replace(/[\0]/g, ' ').trim().slice(0, 1200);
255
- if (!text) throw new AgentProviderError('agent-provider-invalid-response', 'OpenCode Go returned an empty summary.', { status: 502 });
256
- return { summary: text, modelId: model.id };
257
- }
258
-
259
- return { getModels, testConnection, createPlan, createSummary };
260
- }
@@ -1,165 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- const DEFAULT_SERVICE = 'livedesk.agent.opencode-go';
7
- const DEFAULT_ACCOUNT = (() => {
8
- try {
9
- return os.userInfo().username || 'livedesk-user';
10
- } catch {
11
- return 'livedesk-user';
12
- }
13
- })();
14
-
15
- function secretStoreError(message = 'Secure API key storage is unavailable on this system.') {
16
- const error = new Error(message);
17
- error.code = 'agent-secret-store-unavailable';
18
- error.status = 503;
19
- return error;
20
- }
21
- function runCommand(command, args, input = '') {
22
- return new Promise((resolve, reject) => {
23
- const child = spawn(command, args, { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
24
- let stdout = '';
25
- let stderr = '';
26
- child.stdout.setEncoding('utf8');
27
- child.stderr.setEncoding('utf8');
28
- child.stdout.on('data', chunk => { stdout += chunk; });
29
- child.stderr.on('data', chunk => { stderr += chunk; });
30
- child.once('error', () => reject(secretStoreError()));
31
- child.once('close', code => {
32
- if (code === 0) {
33
- resolve(stdout.trim());
34
- } else {
35
- reject(secretStoreError());
36
- }
37
- });
38
- child.stdin.end(input);
39
- });
40
- }
41
-
42
- function providerGuard(provider) {
43
- if (String(provider || '').trim() !== 'opencode-go') {
44
- const error = new Error('Unsupported agent provider.');
45
- error.code = 'agent-unsupported-provider';
46
- error.status = 400;
47
- throw error;
48
- }
49
- }
50
-
51
- function windowsScript(action) {
52
- const source = "$encoded = [Console]::In.ReadToEnd().Trim(); $bytes = [Convert]::FromBase64String($encoded);";
53
- if (action === 'protect') {
54
- return `${source} Add-Type -AssemblyName System.Security; $protected = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser); [Convert]::ToBase64String($protected)`;
55
- }
56
- return `${source} Add-Type -AssemblyName System.Security; $plain = [Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser); [Convert]::ToBase64String($plain)`;
57
- }
58
-
59
- async function protectWindows(value) {
60
- const encoded = Buffer.from(value, 'utf8').toString('base64');
61
- return runCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', windowsScript('protect')], `${encoded}\n`);
62
- }
63
-
64
- async function unprotectWindows(value) {
65
- const encoded = await runCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', windowsScript('unprotect')], `${String(value).trim()}\n`);
66
- try {
67
- return Buffer.from(encoded, 'base64').toString('utf8');
68
- } catch {
69
- throw secretStoreError();
70
- }
71
- }
72
-
73
- export function createOsSecretStore({ dataDir = path.join(os.homedir(), '.livedesk'), service = DEFAULT_SERVICE, account = DEFAULT_ACCOUNT } = {}) {
74
- const dpapiPath = path.join(dataDir, 'agent-opencode-go.dpapi');
75
-
76
- async function setApiKey(provider, value) {
77
- providerGuard(provider);
78
- const apiKey = String(value || '').trim();
79
- if (!apiKey || apiKey.length > 1000) {
80
- const error = new Error('API key is required.');
81
- error.code = 'agent-api-key-invalid';
82
- error.status = 400;
83
- throw error;
84
- }
85
- if (process.platform === 'win32') {
86
- const encrypted = await protectWindows(apiKey);
87
- await mkdir(dataDir, { recursive: true });
88
- const temporaryPath = `${dpapiPath}.${process.pid}.${Date.now()}.tmp`;
89
- await writeFile(temporaryPath, `${encrypted}\n`, { encoding: 'utf8', mode: 0o600 });
90
- await rename(temporaryPath, dpapiPath);
91
- return;
92
- }
93
- if (process.platform === 'darwin') {
94
- await runCommand('security', ['add-generic-password', '-U', '-a', account, '-s', service, '-w', apiKey]);
95
- return;
96
- }
97
- await runCommand('secret-tool', ['store', '--label=LiveDesk Agent API key', 'service', service, 'account', account], `${apiKey}\n`);
98
- }
99
-
100
- async function getApiKey(provider) {
101
- providerGuard(provider);
102
- if (process.platform === 'win32') {
103
- try {
104
- const encrypted = await readFile(dpapiPath, 'utf8');
105
- return await unprotectWindows(encrypted);
106
- } catch (error) {
107
- if (error?.code === 'ENOENT') return null;
108
- throw secretStoreError();
109
- }
110
- }
111
- if (process.platform === 'darwin') {
112
- try {
113
- return await runCommand('security', ['find-generic-password', '-a', account, '-s', service, '-w']);
114
- } catch (error) {
115
- if (error?.code === 'agent-secret-store-unavailable') return null;
116
- throw error;
117
- }
118
- }
119
- try {
120
- return await runCommand('secret-tool', ['lookup', 'service', service, 'account', account]);
121
- } catch {
122
- return null;
123
- }
124
- }
125
-
126
- async function hasApiKey(provider) {
127
- providerGuard(provider);
128
- if (process.platform === 'win32') {
129
- try {
130
- await access(dpapiPath);
131
- return true;
132
- } catch {
133
- return false;
134
- }
135
- }
136
- return Boolean(await getApiKey(provider));
137
- }
138
-
139
- async function deleteApiKey(provider) {
140
- providerGuard(provider);
141
- if (process.platform === 'win32') {
142
- try {
143
- await unlink(dpapiPath);
144
- } catch (error) {
145
- if (error?.code !== 'ENOENT') throw secretStoreError();
146
- }
147
- return;
148
- }
149
- if (process.platform === 'darwin') {
150
- try {
151
- await runCommand('security', ['delete-generic-password', '-a', account, '-s', service]);
152
- } catch {
153
- return;
154
- }
155
- return;
156
- }
157
- try {
158
- await runCommand('secret-tool', ['clear', 'service', service, 'account', account]);
159
- } catch {
160
- return;
161
- }
162
- }
163
-
164
- return { setApiKey, getApiKey, hasApiKey, deleteApiKey };
165
- }