@bytespell/shella 0.1.3 → 0.1.5
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.md +49 -1
- package/bin/cli.js +184 -87
- package/dev/cli.tsx +38 -0
- package/dist/config/openai-codex-models.json +205 -0
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +67 -8
- package/dist/server/lib/opencode-client.d.ts +14 -0
- package/dist/server/lib/opencode-client.js +17 -0
- package/dist/server/lib/opencode-config.d.ts +14 -0
- package/dist/server/lib/opencode-config.js +25 -0
- package/dist/server/routes/config.d.ts +12 -0
- package/dist/server/routes/config.js +207 -0
- package/dist/server/routes/directory.d.ts +8 -0
- package/dist/server/routes/directory.js +84 -0
- package/dist/server/routes/init.d.ts +4 -3
- package/dist/server/routes/init.js +23 -9
- package/dist/server/routes/local-llm.d.ts +8 -0
- package/dist/server/routes/local-llm.js +255 -0
- package/dist/server/routes/logs.js +35 -11
- package/dist/server/routes/prompt.d.ts +16 -0
- package/dist/server/routes/prompt.js +173 -0
- package/dist/server/routes/session.d.ts +8 -0
- package/dist/server/routes/session.js +63 -0
- package/dist/server/routes/status.d.ts +9 -0
- package/dist/server/routes/status.js +54 -0
- package/dist/server/routes/usage.d.ts +12 -0
- package/dist/server/routes/usage.js +60 -0
- package/dist/server/routes/windows.js +4 -4
- package/dist/server/schema.d.ts +47 -16
- package/dist/server/schema.js +8 -1
- package/dist/server/services/database.d.ts +10 -1
- package/dist/server/services/database.js +19 -6
- package/dist/web/assets/{_baseUniq-BXqY9Mam.js → _baseUniq-BxVG561Z.js} +1 -1
- package/dist/web/assets/{arc-Bn6tUpO_.js → arc-B9TFF79T.js} +1 -1
- package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BMRLMpf8.js} +1 -1
- package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-DBQKFxeQ.js} +1 -1
- package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-TiYEZrdu.js} +1 -1
- package/dist/web/assets/channel-aOIIaiSs.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-CWCp0N17.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CeVCFKqv.js} +1 -1
- package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-B-AoaHJt.js} +1 -1
- package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-CCGkXnX-.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-Bcupjeb_.js} +1 -1
- package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-DlyUQaTO.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-THN-at_3.js} +1 -1
- package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-CtErOFJM.js} +1 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-BFLMv18M.js +1 -0
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-BFLMv18M.js +1 -0
- package/dist/web/assets/clone-BR_FHSwu.js +1 -0
- package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-CPI-88R6.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-YrHmsLe4.js} +1 -1
- package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-Dcvw3qhj.js} +1 -1
- package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-B135EOe6.js} +1 -1
- package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-w3KdB_-u.js} +1 -1
- package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DYssvOTP.js} +1 -1
- package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-DpnuE7B_.js} +1 -1
- package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-BhcJ-8Yu.js} +1 -1
- package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-ButVkRCz.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-ZLz8eoSo.js} +1 -1
- package/dist/web/assets/{graph-DqhaNOTU.js → graph-CKmCFGqF.js} +1 -1
- package/dist/web/assets/index-BHJDUcNL.js +1719 -0
- package/dist/web/assets/index-CcAJUkQw.css +1 -0
- package/dist/web/assets/index-DEiKajXR.js +1 -0
- package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-C_h94brE.js} +1 -1
- package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-u0bPRxxb.js} +1 -1
- package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-DkM-KD6Y.js} +1 -1
- package/dist/web/assets/{layout-Cc1ESzTe.js → layout-DGSU3MQw.js} +1 -1
- package/dist/web/assets/{linear-BwI2ANFG.js → linear-Dck9QCb9.js} +1 -1
- package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-DfB-jqaz.js} +5 -5
- package/dist/web/assets/{min--MKscDc6.js → min-CqEcl9J0.js} +1 -1
- package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-D1KrSALz.js} +1 -1
- package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-CZ-507Bd.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-Jw0og6Ix.js} +1 -1
- package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-5JWD7TEH.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-DzlxPj37.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-Cui1ykiA.js} +1 -1
- package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-CCdGE_zt.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CsLg9bzy.js +1 -0
- package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-CP2T8mHI.js} +1 -1
- package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-DGBVlHVf.js} +1 -1
- package/dist/web/assets/welcome-screen-test-DnIwI3hf.js +1 -0
- package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-DDlMipkA.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +14 -5
- package/dist/web/assets/channel-CxjnQtV7.js +0 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-CVG91-fs.js +0 -1
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-CVG91-fs.js +0 -1
- package/dist/web/assets/clone-C7jxvixc.js +0 -1
- package/dist/web/assets/index-B0jWvqrS.css +0 -1
- package/dist/web/assets/index-Dnmavb3d.js +0 -1716
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-oHTO1yj_.js +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory API Routes
|
|
3
|
+
*
|
|
4
|
+
* Provides aggregated directory metadata by calling OpenCode SDK server-side.
|
|
5
|
+
* Returns directory information including VCS branch, diffs, file counts, etc.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { opencodeClient } from '../lib/opencode-client.js';
|
|
9
|
+
const router = Router();
|
|
10
|
+
/**
|
|
11
|
+
* GET /directory/:directoryId
|
|
12
|
+
*
|
|
13
|
+
* Returns aggregated directory information:
|
|
14
|
+
* - path (from window directoryMap lookup)
|
|
15
|
+
* - branch (from OpenCode VCS API)
|
|
16
|
+
* - diff (STUB - null for now)
|
|
17
|
+
* - fileCount (STUB - null for now)
|
|
18
|
+
* - lastModified (STUB - null for now)
|
|
19
|
+
*/
|
|
20
|
+
router.get('/directory/:directoryId', async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const { directoryId } = req.params;
|
|
23
|
+
if (!directoryId) {
|
|
24
|
+
res.status(400).json({ error: 'directoryId is required' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Get directory path from windows table
|
|
28
|
+
// Find any window with this directoryId and extract the directory from it
|
|
29
|
+
// Note: We need to query project.current or project.list from OpenCode SDK
|
|
30
|
+
// For now, we'll use a simpler approach: query OpenCode SDK for project list
|
|
31
|
+
// Get all projects from OpenCode
|
|
32
|
+
const projectsResponse = await opencodeClient.project.list();
|
|
33
|
+
const projects = projectsResponse.data || [];
|
|
34
|
+
let directoryPath = null;
|
|
35
|
+
// Match project by directoryId (which is the worktree path)
|
|
36
|
+
if (projects.length > 0) {
|
|
37
|
+
const matchingProject = projects.find((p) => p.worktree === directoryId);
|
|
38
|
+
if (matchingProject) {
|
|
39
|
+
directoryPath = matchingProject.worktree;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fallback: use directoryId as path (works in simple cases)
|
|
43
|
+
if (!directoryPath) {
|
|
44
|
+
directoryPath = directoryId;
|
|
45
|
+
}
|
|
46
|
+
// Fetch VCS info from OpenCode SDK
|
|
47
|
+
let branch = null;
|
|
48
|
+
try {
|
|
49
|
+
const vcsResponse = await opencodeClient.vcs.get({
|
|
50
|
+
directory: directoryPath,
|
|
51
|
+
});
|
|
52
|
+
branch = vcsResponse.data?.branch || null;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// VCS call failed - might not be a git repo, or path doesn't exist
|
|
56
|
+
console.warn(`[directory] VCS fetch failed for ${directoryPath}:`, err);
|
|
57
|
+
branch = null;
|
|
58
|
+
}
|
|
59
|
+
// TODO: Fetch diff stats (git status)
|
|
60
|
+
// const diffResponse = await opencodeClient.file.status({ directory: directoryPath });
|
|
61
|
+
const diff = null; // STUB
|
|
62
|
+
// TODO: Fetch file count
|
|
63
|
+
// const filesResponse = await opencodeClient.file.list({ directory: directoryPath });
|
|
64
|
+
const fileCount = null; // STUB
|
|
65
|
+
// TODO: Fetch last modified time
|
|
66
|
+
// Could use fs.stat on the directory or git log
|
|
67
|
+
const lastModified = null; // STUB
|
|
68
|
+
// Return aggregated data
|
|
69
|
+
res.json({
|
|
70
|
+
path: directoryPath,
|
|
71
|
+
branch,
|
|
72
|
+
diff,
|
|
73
|
+
fileCount,
|
|
74
|
+
lastModified,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('[directory] API error:', error);
|
|
79
|
+
res.status(500).json({
|
|
80
|
+
error: error instanceof Error ? error.message : 'Internal server error',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
export default router;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* shella initialization endpoint
|
|
3
3
|
*
|
|
4
4
|
* TODO: Consolidate more startup data here to reduce round-trips:
|
|
5
5
|
* - windows (currently /__shella/windows)
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - agents, config, providers (currently /api/* via OpenCode proxy)
|
|
9
9
|
* - permissions (currently /api/permission via OpenCode proxy)
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Returns mode-specific information:
|
|
12
|
+
* - cwd mode: directory info for the single working directory
|
|
13
|
+
* - server mode: projectsDir info for multi-project setup
|
|
13
14
|
*/
|
|
14
15
|
declare const router: import("express-serve-static-core").Router;
|
|
15
16
|
export default router;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* shella initialization endpoint
|
|
3
3
|
*
|
|
4
4
|
* TODO: Consolidate more startup data here to reduce round-trips:
|
|
5
5
|
* - windows (currently /__shella/windows)
|
|
@@ -8,18 +8,32 @@
|
|
|
8
8
|
* - agents, config, providers (currently /api/* via OpenCode proxy)
|
|
9
9
|
* - permissions (currently /api/permission via OpenCode proxy)
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Returns mode-specific information:
|
|
12
|
+
* - cwd mode: directory info for the single working directory
|
|
13
|
+
* - server mode: projectsDir info for multi-project setup
|
|
13
14
|
*/
|
|
14
15
|
import { Router } from 'express';
|
|
15
16
|
import path from 'path';
|
|
16
|
-
import { PROJECTS_DIR, VERSION } from '../index.js';
|
|
17
|
+
import { MODE, DIRECTORY, PROJECTS_DIR, VERSION } from '../index.js';
|
|
17
18
|
const router = Router();
|
|
18
19
|
router.get('/', (_req, res) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if (MODE === 'cwd') {
|
|
21
|
+
// cwd mode: single directory
|
|
22
|
+
res.json({
|
|
23
|
+
mode: 'cwd',
|
|
24
|
+
directory: DIRECTORY,
|
|
25
|
+
directoryName: DIRECTORY ? path.basename(DIRECTORY) : null,
|
|
26
|
+
version: VERSION,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// server mode: multi-project
|
|
31
|
+
res.json({
|
|
32
|
+
mode: 'server',
|
|
33
|
+
projectsDir: PROJECTS_DIR,
|
|
34
|
+
projectsDirName: PROJECTS_DIR ? path.basename(PROJECTS_DIR) : null,
|
|
35
|
+
version: VERSION,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
24
38
|
});
|
|
25
39
|
export default router;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local LLM Routes
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for validating and adding OpenAI-compatible local LLM providers
|
|
5
|
+
* (LM Studio, Ollama, llama.cpp, etc.) to the global OpenCode config.
|
|
6
|
+
*/
|
|
7
|
+
declare const router: import("express-serve-static-core").Router;
|
|
8
|
+
export default router;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local LLM Routes
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for validating and adding OpenAI-compatible local LLM providers
|
|
5
|
+
* (LM Studio, Ollama, llama.cpp, etc.) to the global OpenCode config.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { opencodeClient } from '../lib/opencode-client.js';
|
|
12
|
+
const router = Router();
|
|
13
|
+
/** Global OpenCode config path */
|
|
14
|
+
const OPENCODE_CONFIG_DIR = join(homedir(), '.config', 'opencode');
|
|
15
|
+
const OPENCODE_CONFIG_PATH = join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
|
16
|
+
/** Timeout for validation requests (5 seconds) */
|
|
17
|
+
const VALIDATION_TIMEOUT_MS = 5000;
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize provider name to create a valid provider ID.
|
|
20
|
+
* "LM Studio" -> "lm-studio"
|
|
21
|
+
* "My Custom Provider!" -> "my-custom-provider"
|
|
22
|
+
*/
|
|
23
|
+
function sanitizeProviderId(name) {
|
|
24
|
+
return name
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
28
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
29
|
+
.substring(0, 50); // Limit length
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the global OpenCode config file.
|
|
33
|
+
* Returns empty config structure if file doesn't exist.
|
|
34
|
+
*/
|
|
35
|
+
function readOpencodeConfig() {
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
38
|
+
const content = readFileSync(OPENCODE_CONFIG_PATH, 'utf-8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('[local-llm] Failed to read opencode.json:', err);
|
|
44
|
+
}
|
|
45
|
+
// Return default config structure
|
|
46
|
+
return {
|
|
47
|
+
$schema: 'https://opencode.ai/config.json',
|
|
48
|
+
provider: {},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Write the global OpenCode config file.
|
|
53
|
+
*/
|
|
54
|
+
function writeOpencodeConfig(config) {
|
|
55
|
+
// Ensure config directory exists
|
|
56
|
+
if (!existsSync(OPENCODE_CONFIG_DIR)) {
|
|
57
|
+
mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const content = JSON.stringify(config, null, 2);
|
|
60
|
+
writeFileSync(OPENCODE_CONFIG_PATH, content, 'utf-8');
|
|
61
|
+
console.log(`[local-llm] Wrote config to ${OPENCODE_CONFIG_PATH}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* POST /validate
|
|
65
|
+
*
|
|
66
|
+
* Validates a local LLM server by fetching its /models endpoint.
|
|
67
|
+
* Returns the list of available models if successful.
|
|
68
|
+
*
|
|
69
|
+
* Request body: { baseUrl: string }
|
|
70
|
+
* Response: { success: true, models: [...] } or { success: false, error: "..." }
|
|
71
|
+
*/
|
|
72
|
+
router.post('/validate', async (req, res) => {
|
|
73
|
+
const { baseUrl } = req.body;
|
|
74
|
+
if (!baseUrl || typeof baseUrl !== 'string') {
|
|
75
|
+
res.status(400).json({ success: false, error: 'baseUrl is required' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Normalize URL - remove trailing slash
|
|
79
|
+
const normalizedUrl = baseUrl.replace(/\/+$/, '');
|
|
80
|
+
// Construct models endpoint
|
|
81
|
+
const modelsUrl = normalizedUrl.endsWith('/v1')
|
|
82
|
+
? `${normalizedUrl}/models`
|
|
83
|
+
: `${normalizedUrl}/v1/models`;
|
|
84
|
+
console.log(`[local-llm] Validating: ${modelsUrl}`);
|
|
85
|
+
try {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
|
|
88
|
+
const response = await fetch(modelsUrl, {
|
|
89
|
+
method: 'GET',
|
|
90
|
+
headers: {
|
|
91
|
+
Accept: 'application/json',
|
|
92
|
+
},
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
});
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const text = await response.text().catch(() => '');
|
|
98
|
+
res.json({
|
|
99
|
+
success: false,
|
|
100
|
+
error: `Server returned ${response.status}: ${text || response.statusText}`,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const data = (await response.json());
|
|
105
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
106
|
+
res.json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: 'Invalid response format: expected { data: [...] }',
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Extract model info
|
|
113
|
+
const models = data.data.map((model) => ({
|
|
114
|
+
id: model.id,
|
|
115
|
+
name: model.id, // Use ID as name by default
|
|
116
|
+
owned_by: model.owned_by,
|
|
117
|
+
}));
|
|
118
|
+
console.log(`[local-llm] Found ${models.length} model(s)`);
|
|
119
|
+
res.json({ success: true, models });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
const errorCode = err?.code;
|
|
124
|
+
console.log(`[local-llm] Validation failed: ${message} (code: ${errorCode})`);
|
|
125
|
+
// Provide user-friendly error messages
|
|
126
|
+
let userError = message;
|
|
127
|
+
if (errorCode === 'ECONNREFUSED' || message.includes('ECONNREFUSED')) {
|
|
128
|
+
userError = 'Connection refused. Is the server running?';
|
|
129
|
+
}
|
|
130
|
+
else if (errorCode === 'ENOTFOUND' || message.includes('ENOTFOUND')) {
|
|
131
|
+
userError = 'Host not found. Check the URL.';
|
|
132
|
+
}
|
|
133
|
+
else if (errorCode === 'ETIMEDOUT' ||
|
|
134
|
+
message.includes('abort') ||
|
|
135
|
+
message.includes('timeout')) {
|
|
136
|
+
userError = 'Connection timed out after 5 seconds';
|
|
137
|
+
}
|
|
138
|
+
else if (message === 'fetch failed' || message.includes('fetch')) {
|
|
139
|
+
userError = 'Connection failed. Is the server running at this address?';
|
|
140
|
+
}
|
|
141
|
+
res.json({ success: false, error: userError });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* POST /add
|
|
146
|
+
*
|
|
147
|
+
* Adds a local LLM provider to the global OpenCode config.
|
|
148
|
+
*
|
|
149
|
+
* Request body: { name: string, baseUrl: string, models: { id: string, name: string }[] }
|
|
150
|
+
* Response: { success: true, providerId: "..." } or { success: false, error: "..." }
|
|
151
|
+
*/
|
|
152
|
+
router.post('/add', async (req, res) => {
|
|
153
|
+
const { name, baseUrl, models } = req.body;
|
|
154
|
+
if (!name || typeof name !== 'string') {
|
|
155
|
+
res.status(400).json({ success: false, error: 'name is required' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!baseUrl || typeof baseUrl !== 'string') {
|
|
159
|
+
res.status(400).json({ success: false, error: 'baseUrl is required' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!models || !Array.isArray(models) || models.length === 0) {
|
|
163
|
+
res.status(400).json({ success: false, error: 'At least one model is required' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const providerId = sanitizeProviderId(name);
|
|
167
|
+
if (!providerId) {
|
|
168
|
+
res.status(400).json({ success: false, error: 'Invalid provider name' });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Read existing config
|
|
172
|
+
const config = readOpencodeConfig();
|
|
173
|
+
// Check if provider already exists
|
|
174
|
+
if (config.provider?.[providerId]) {
|
|
175
|
+
res.status(409).json({
|
|
176
|
+
success: false,
|
|
177
|
+
error: `Provider "${providerId}" already exists`,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Normalize URL - ensure it ends with /v1
|
|
182
|
+
let normalizedUrl = baseUrl.replace(/\/+$/, '');
|
|
183
|
+
if (!normalizedUrl.endsWith('/v1')) {
|
|
184
|
+
normalizedUrl = `${normalizedUrl}/v1`;
|
|
185
|
+
}
|
|
186
|
+
// Build models config
|
|
187
|
+
const modelsConfig = {};
|
|
188
|
+
for (const model of models) {
|
|
189
|
+
modelsConfig[model.id] = { name: model.name };
|
|
190
|
+
}
|
|
191
|
+
// Create provider config
|
|
192
|
+
const providerConfig = {
|
|
193
|
+
npm: '@ai-sdk/openai-compatible',
|
|
194
|
+
name: name.trim(),
|
|
195
|
+
options: {
|
|
196
|
+
baseURL: normalizedUrl,
|
|
197
|
+
},
|
|
198
|
+
models: modelsConfig,
|
|
199
|
+
};
|
|
200
|
+
// Merge into config
|
|
201
|
+
if (!config.provider) {
|
|
202
|
+
config.provider = {};
|
|
203
|
+
}
|
|
204
|
+
config.provider[providerId] = providerConfig;
|
|
205
|
+
try {
|
|
206
|
+
// Write config
|
|
207
|
+
writeOpencodeConfig(config);
|
|
208
|
+
// Force OpenCode to reload by disposing the current instance
|
|
209
|
+
// This will cause the next request to pick up the new config
|
|
210
|
+
try {
|
|
211
|
+
await opencodeClient.global.dispose();
|
|
212
|
+
console.log('[local-llm] Disposed OpenCode instance to reload config');
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Non-fatal - OpenCode may not be running yet
|
|
216
|
+
console.log('[local-llm] Could not dispose OpenCode instance (may not be running)');
|
|
217
|
+
}
|
|
218
|
+
res.json({ success: true, providerId });
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
222
|
+
console.error('[local-llm] Failed to write config:', message);
|
|
223
|
+
res.status(500).json({ success: false, error: 'Failed to save configuration' });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
/**
|
|
227
|
+
* GET /list
|
|
228
|
+
*
|
|
229
|
+
* Lists all local LLM providers from the global OpenCode config.
|
|
230
|
+
*
|
|
231
|
+
* Response: { providers: [{ id: string, name: string, baseUrl: string, models: [...] }] }
|
|
232
|
+
*/
|
|
233
|
+
router.get('/list', (_req, res) => {
|
|
234
|
+
const config = readOpencodeConfig();
|
|
235
|
+
const providers = [];
|
|
236
|
+
if (config.provider) {
|
|
237
|
+
for (const [id, providerConfig] of Object.entries(config.provider)) {
|
|
238
|
+
// Only include local LLM providers (those using @ai-sdk/openai-compatible)
|
|
239
|
+
if (providerConfig.npm === '@ai-sdk/openai-compatible') {
|
|
240
|
+
const models = Object.entries(providerConfig.models || {}).map(([modelId, modelInfo]) => ({
|
|
241
|
+
id: modelId,
|
|
242
|
+
name: modelInfo.name,
|
|
243
|
+
}));
|
|
244
|
+
providers.push({
|
|
245
|
+
id,
|
|
246
|
+
name: providerConfig.name,
|
|
247
|
+
baseUrl: providerConfig.options?.baseURL || '',
|
|
248
|
+
models,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
res.json({ providers });
|
|
254
|
+
});
|
|
255
|
+
export default router;
|
|
@@ -3,26 +3,50 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
const LOG_FILE = path.resolve(process.cwd(), 'dev.log');
|
|
5
5
|
const router = Router();
|
|
6
|
-
//
|
|
6
|
+
// Pino log levels
|
|
7
|
+
const LEVEL_MAP = {
|
|
8
|
+
error: 50,
|
|
9
|
+
warn: 40,
|
|
10
|
+
info: 30,
|
|
11
|
+
log: 30,
|
|
12
|
+
debug: 20,
|
|
13
|
+
};
|
|
14
|
+
// POST /__logs - receive browser console logs (JSON format for pino compatibility)
|
|
7
15
|
router.post('/', (req, res) => {
|
|
8
16
|
try {
|
|
9
17
|
const { level, args } = req.body;
|
|
10
|
-
const
|
|
11
|
-
const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
18
|
+
const pinoLevel = LEVEL_MAP[level] ?? 30;
|
|
12
19
|
const message = args
|
|
13
20
|
.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a)))
|
|
14
21
|
.join(' ');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// JSON log entry compatible with pino format
|
|
23
|
+
const entry = {
|
|
24
|
+
level: pinoLevel,
|
|
25
|
+
time: Date.now(),
|
|
26
|
+
source: 'browser',
|
|
27
|
+
msg: message,
|
|
28
|
+
};
|
|
29
|
+
// Pretty print to terminal
|
|
30
|
+
const prefix = level === 'error'
|
|
31
|
+
? '\x1b[31m●\x1b[0m'
|
|
32
|
+
: level === 'warn'
|
|
33
|
+
? '\x1b[33m●\x1b[0m'
|
|
34
|
+
: '\x1b[36m●\x1b[0m';
|
|
35
|
+
const timestamp = new Date().toISOString().slice(11, 23);
|
|
36
|
+
console.log(`${timestamp} ${prefix} [browser] ${message}`);
|
|
37
|
+
// Append JSON to file
|
|
38
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
20
39
|
res.sendStatus(200);
|
|
21
40
|
}
|
|
22
41
|
catch {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
const entry = {
|
|
43
|
+
level: 40,
|
|
44
|
+
time: Date.now(),
|
|
45
|
+
source: 'browser',
|
|
46
|
+
msg: `[parse error] ${JSON.stringify(req.body)}`,
|
|
47
|
+
};
|
|
48
|
+
console.log(`\x1b[33m●\x1b[0m [browser] ${entry.msg}`);
|
|
49
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
26
50
|
res.sendStatus(200);
|
|
27
51
|
}
|
|
28
52
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shella Prompt Routes
|
|
3
|
+
*
|
|
4
|
+
* Handles prompt, command, and shell execution with integrated auto-routing.
|
|
5
|
+
* All __auto__ model resolution happens server-side - no extra HTTP calls needed.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* POST /prompt - Submit a prompt (message)
|
|
9
|
+
* POST /command - Execute a slash command
|
|
10
|
+
* POST /shell - Execute a shell command
|
|
11
|
+
*
|
|
12
|
+
* All endpoints return 202 immediately with routing info.
|
|
13
|
+
* The actual OpenCode SDK call is fire-and-forget.
|
|
14
|
+
*/
|
|
15
|
+
declare const router: import("express-serve-static-core").Router;
|
|
16
|
+
export default router;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shella Prompt Routes
|
|
3
|
+
*
|
|
4
|
+
* Handles prompt, command, and shell execution with integrated auto-routing.
|
|
5
|
+
* All __auto__ model resolution happens server-side - no extra HTTP calls needed.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* POST /prompt - Submit a prompt (message)
|
|
9
|
+
* POST /command - Execute a slash command
|
|
10
|
+
* POST /shell - Execute a shell command
|
|
11
|
+
*
|
|
12
|
+
* All endpoints return 202 immediately with routing info.
|
|
13
|
+
* The actual OpenCode SDK call is fire-and-forget.
|
|
14
|
+
*/
|
|
15
|
+
import { Router } from 'express';
|
|
16
|
+
import { pickBestProvider, getUsage, } from '@bytespell/model-provider-usage-limits';
|
|
17
|
+
import { opencodeClient } from '../lib/opencode-client.js';
|
|
18
|
+
const router = Router();
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Auto-Route Resolution
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Infer default provider from model name pattern.
|
|
24
|
+
* Used as fallback when routing fails.
|
|
25
|
+
*/
|
|
26
|
+
function inferDefaultProvider(modelID) {
|
|
27
|
+
// GPT models, o-series (o1, o3, etc.), and codex -> openai
|
|
28
|
+
if (modelID.startsWith('gpt') || /^o\d/.test(modelID) || modelID.startsWith('codex')) {
|
|
29
|
+
return 'openai';
|
|
30
|
+
}
|
|
31
|
+
// Everything else (claude, etc.) -> anthropic
|
|
32
|
+
return 'anthropic';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Server-side auto-route resolution.
|
|
36
|
+
* No HTTP call needed - direct function call to usage-limits library.
|
|
37
|
+
*/
|
|
38
|
+
async function resolveModel(model) {
|
|
39
|
+
// Not an auto model - return as-is
|
|
40
|
+
if (model.providerID !== '__auto__') {
|
|
41
|
+
return { resolved: model, routed: false };
|
|
42
|
+
}
|
|
43
|
+
const defaultProvider = inferDefaultProvider(model.modelID);
|
|
44
|
+
try {
|
|
45
|
+
const usageData = await getUsage({ autoDetectAuthTokens: true });
|
|
46
|
+
const result = pickBestProvider({ providerID: defaultProvider, modelID: model.modelID }, usageData);
|
|
47
|
+
if (result) {
|
|
48
|
+
return {
|
|
49
|
+
resolved: { providerID: result.providerID, modelID: result.modelID },
|
|
50
|
+
routed: true,
|
|
51
|
+
reason: result.reason,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
console.error('[prompt] Routing error:', message);
|
|
58
|
+
}
|
|
59
|
+
// Fallback to default provider
|
|
60
|
+
return {
|
|
61
|
+
resolved: { providerID: defaultProvider, modelID: model.modelID },
|
|
62
|
+
routed: true,
|
|
63
|
+
reason: 'Fallback to default provider',
|
|
64
|
+
fallback: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Routes
|
|
69
|
+
// =============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* POST /prompt
|
|
72
|
+
*
|
|
73
|
+
* Submit a prompt with auto-routing support.
|
|
74
|
+
* Returns 202 immediately with routing info, fires request to OpenCode async.
|
|
75
|
+
*/
|
|
76
|
+
router.post('/prompt', async (req, res) => {
|
|
77
|
+
const { sessionID, directory, messageID, parts, model, agent } = req.body;
|
|
78
|
+
if (!sessionID || !directory || !model) {
|
|
79
|
+
res.status(400).json({ error: 'Missing required fields: sessionID, directory, model' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Resolve model (handles __auto__)
|
|
83
|
+
const routeResult = await resolveModel(model);
|
|
84
|
+
console.log(`[prompt] ${model.providerID}/${model.modelID} → ${routeResult.resolved.providerID}/${routeResult.resolved.modelID}`);
|
|
85
|
+
// Fire-and-forget to OpenCode
|
|
86
|
+
opencodeClient.session
|
|
87
|
+
.promptAsync({
|
|
88
|
+
sessionID,
|
|
89
|
+
directory,
|
|
90
|
+
messageID,
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
parts: parts, // Parts are validated by the SDK
|
|
93
|
+
model: routeResult.resolved,
|
|
94
|
+
agent,
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
console.error('[prompt] SDK error:', err);
|
|
98
|
+
});
|
|
99
|
+
// Return immediately with routing info
|
|
100
|
+
res.status(202).json({
|
|
101
|
+
accepted: true,
|
|
102
|
+
...routeResult,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* POST /command
|
|
107
|
+
*
|
|
108
|
+
* Execute a slash command with auto-routing support.
|
|
109
|
+
*/
|
|
110
|
+
router.post('/command', async (req, res) => {
|
|
111
|
+
const { sessionID, directory, command, args, model, agent } = req.body;
|
|
112
|
+
if (!sessionID || !directory || !command || !model) {
|
|
113
|
+
res
|
|
114
|
+
.status(400)
|
|
115
|
+
.json({ error: 'Missing required fields: sessionID, directory, command, model' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const routeResult = await resolveModel(model);
|
|
119
|
+
console.log(`[command] /${command} → ${routeResult.resolved.providerID}/${routeResult.resolved.modelID}`);
|
|
120
|
+
// Fire-and-forget to OpenCode
|
|
121
|
+
// Note: SDK command() expects model as string "{providerID}/{modelID}"
|
|
122
|
+
const modelString = `${routeResult.resolved.providerID}/${routeResult.resolved.modelID}`;
|
|
123
|
+
opencodeClient.session
|
|
124
|
+
.command({
|
|
125
|
+
sessionID,
|
|
126
|
+
directory,
|
|
127
|
+
command,
|
|
128
|
+
arguments: args,
|
|
129
|
+
model: modelString,
|
|
130
|
+
agent,
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => {
|
|
133
|
+
console.error('[command] SDK error:', err);
|
|
134
|
+
});
|
|
135
|
+
res.status(202).json({
|
|
136
|
+
accepted: true,
|
|
137
|
+
...routeResult,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* POST /shell
|
|
142
|
+
*
|
|
143
|
+
* Execute a shell command with auto-routing support.
|
|
144
|
+
*/
|
|
145
|
+
router.post('/shell', async (req, res) => {
|
|
146
|
+
const { sessionID, directory, command, model, agent } = req.body;
|
|
147
|
+
if (!sessionID || !directory || !command || !model) {
|
|
148
|
+
res
|
|
149
|
+
.status(400)
|
|
150
|
+
.json({ error: 'Missing required fields: sessionID, directory, command, model' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const routeResult = await resolveModel(model);
|
|
154
|
+
const truncatedCmd = command.length > 30 ? `${command.slice(0, 30)}...` : command;
|
|
155
|
+
console.log(`[shell] !${truncatedCmd} → ${routeResult.resolved.providerID}/${routeResult.resolved.modelID}`);
|
|
156
|
+
// Fire-and-forget to OpenCode
|
|
157
|
+
opencodeClient.session
|
|
158
|
+
.shell({
|
|
159
|
+
sessionID,
|
|
160
|
+
directory,
|
|
161
|
+
command,
|
|
162
|
+
model: routeResult.resolved,
|
|
163
|
+
agent,
|
|
164
|
+
})
|
|
165
|
+
.catch((err) => {
|
|
166
|
+
console.error('[shell] SDK error:', err);
|
|
167
|
+
});
|
|
168
|
+
res.status(202).json({
|
|
169
|
+
accepted: true,
|
|
170
|
+
...routeResult,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
export default router;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Routes
|
|
3
|
+
*
|
|
4
|
+
* Intercepts OpenCode session endpoints to apply shella-specific transformations.
|
|
5
|
+
* Currently handles message chunking on compaction boundaries.
|
|
6
|
+
*/
|
|
7
|
+
declare const router: import("express-serve-static-core").Router;
|
|
8
|
+
export default router;
|