@bytespell/shella 0.1.3 → 0.1.4
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 +1 -1
- package/bin/cli.js +153 -82
- package/dev/cli.tsx +26 -0
- package/dist/config/openai-codex-models.json +205 -0
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +66 -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/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-6T01QAux.js} +1 -1
- package/dist/web/assets/{arc-Bn6tUpO_.js → arc-BkH3TPJb.js} +1 -1
- package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BSi6BLCC.js} +1 -1
- package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-QSPUbinO.js} +1 -1
- package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-Cya_BihR.js} +1 -1
- package/dist/web/assets/channel-DGAtS-pa.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-DIL6eizv.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CgwejoZz.js} +1 -1
- package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-9mIPqoGe.js} +1 -1
- package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-BRbyRfgT.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-CVBT25Fj.js} +1 -1
- package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-rTj-WT2G.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-BaUBiHya.js} +1 -1
- package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-C4_O5TI-.js} +1 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-DLvlUUJq.js +1 -0
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLvlUUJq.js +1 -0
- package/dist/web/assets/clone-BZW2JABw.js +1 -0
- package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-Bj_2OIYt.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-T7a1luWi.js} +1 -1
- package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-CeH5ZsdW.js} +1 -1
- package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-Cdod2Lna.js} +1 -1
- package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-CYks2r54.js} +1 -1
- package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DCmy0g7p.js} +1 -1
- package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-Dlz1bNvI.js} +1 -1
- package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-Di5Iit1B.js} +1 -1
- package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-9i1dugg-.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-BORbMVri.js} +1 -1
- package/dist/web/assets/{graph-DqhaNOTU.js → graph-C0SCKxbQ.js} +1 -1
- package/dist/web/assets/index-CYVJT8rN.js +1 -0
- package/dist/web/assets/index-CcAJUkQw.css +1 -0
- package/dist/web/assets/index-CcDdxbB-.js +1719 -0
- package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-7ohMQFLY.js} +1 -1
- package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-DZp7Z7wE.js} +1 -1
- package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-BCNLCm54.js} +1 -1
- package/dist/web/assets/{layout-Cc1ESzTe.js → layout-AUnZuY21.js} +1 -1
- package/dist/web/assets/{linear-BwI2ANFG.js → linear-B0bfAqGt.js} +1 -1
- package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-D5fXNCxA.js} +5 -5
- package/dist/web/assets/{min--MKscDc6.js → min-BZUFOEEw.js} +1 -1
- package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-hEGJLJ8N.js} +1 -1
- package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-BRpCTJIO.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-m7jaiHQb.js} +1 -1
- package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-Coh9g9Sp.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-CrD_kUGR.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-C04yD1EI.js} +1 -1
- package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-DhP-DMZW.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-DWi5vrD6.js +1 -0
- package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-40iW2p_5.js} +1 -1
- package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-BnxWQbzt.js} +1 -1
- package/dist/web/assets/welcome-screen-test-CLeWuIqq.js +1 -0
- package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-D6lcJDCc.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
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,13 @@ import windowsRoutes from './routes/windows.js';
|
|
|
9
9
|
import modelStateRoutes from './routes/model-state.js';
|
|
10
10
|
import logsRoutes from './routes/logs.js';
|
|
11
11
|
import proxyRoutes from './routes/proxy.js';
|
|
12
|
+
import sessionRoutes from './routes/session.js';
|
|
12
13
|
import initRoutes from './routes/init.js';
|
|
14
|
+
import statusRoutes from './routes/status.js';
|
|
15
|
+
import usageRoutes from './routes/usage.js';
|
|
16
|
+
import configRoutes from './routes/config.js';
|
|
17
|
+
import promptRoutes from './routes/prompt.js';
|
|
18
|
+
import localLlmRoutes from './routes/local-llm.js';
|
|
13
19
|
import { getDatabase } from './services/database.js';
|
|
14
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
21
|
// Runtime mode detection
|
|
@@ -20,14 +26,50 @@ if (!isProd) {
|
|
|
20
26
|
}
|
|
21
27
|
// Port: 3067 in prod, 3068 in dev (Vite uses 3067 in dev)
|
|
22
28
|
const PORT = process.env.PORT || (isProd ? 3067 : 3068);
|
|
23
|
-
// Config directory: ~/.config/shella/
|
|
24
|
-
|
|
29
|
+
// Config directory: SHELLA_DATA_DIR env var, or ~/.config/shella/ by default
|
|
30
|
+
// In Docker, SHELLA_DATA_DIR is set to /projects/.shella
|
|
31
|
+
const CONFIG_DIR = process.env.SHELLA_DATA_DIR
|
|
32
|
+
? path.resolve(process.env.SHELLA_DATA_DIR)
|
|
33
|
+
: path.join(os.homedir(), '.config', 'shella');
|
|
25
34
|
// Log file: ~/.config/shella/shella.log in prod, ./dev.log in dev
|
|
26
35
|
const LOG_FILE = isProd
|
|
27
36
|
? path.join(CONFIG_DIR, 'shella.log')
|
|
28
37
|
: path.resolve(process.cwd(), 'dev.log');
|
|
29
38
|
/**
|
|
30
|
-
* Parse --
|
|
39
|
+
* Parse --mode from CLI args
|
|
40
|
+
* 'cwd' mode: run in current directory only
|
|
41
|
+
* 'server' mode: register all subdirectories as projects
|
|
42
|
+
*/
|
|
43
|
+
function parseMode() {
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
const idx = args.indexOf('--mode');
|
|
46
|
+
if (idx !== -1 && args[idx + 1]) {
|
|
47
|
+
const mode = args[idx + 1];
|
|
48
|
+
if (mode === 'cwd' || mode === 'server') {
|
|
49
|
+
return mode;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Fall back to env var (for dev mode)
|
|
53
|
+
if (process.env.SHELLA_MODE === 'cwd' || process.env.SHELLA_MODE === 'server') {
|
|
54
|
+
return process.env.SHELLA_MODE;
|
|
55
|
+
}
|
|
56
|
+
// Default to 'server' for backwards compatibility
|
|
57
|
+
return 'server';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse --directory from CLI args (cwd mode)
|
|
61
|
+
*/
|
|
62
|
+
function parseDirectory() {
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
const idx = args.indexOf('--directory');
|
|
65
|
+
if (idx !== -1 && args[idx + 1]) {
|
|
66
|
+
return path.resolve(args[idx + 1]);
|
|
67
|
+
}
|
|
68
|
+
// Default to cwd
|
|
69
|
+
return process.cwd();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse --projects-dir from CLI args (server mode)
|
|
31
73
|
*/
|
|
32
74
|
function parseProjectsDir() {
|
|
33
75
|
const args = process.argv.slice(2);
|
|
@@ -42,10 +84,14 @@ function parseProjectsDir() {
|
|
|
42
84
|
// Default to cwd
|
|
43
85
|
return process.cwd();
|
|
44
86
|
}
|
|
45
|
-
//
|
|
46
|
-
export const
|
|
87
|
+
// Mode: 'cwd' or 'server'
|
|
88
|
+
export const MODE = parseMode();
|
|
89
|
+
// Directory - the single directory for cwd mode
|
|
90
|
+
export const DIRECTORY = MODE === 'cwd' ? parseDirectory() : null;
|
|
91
|
+
// Projects directory - passed from CLI or .env.local (server mode)
|
|
92
|
+
export const PROJECTS_DIR = MODE === 'server' ? parseProjectsDir() : null;
|
|
47
93
|
// Version - keep in sync with package.json
|
|
48
|
-
export const VERSION = '0.1.
|
|
94
|
+
export const VERSION = '0.1.4';
|
|
49
95
|
/**
|
|
50
96
|
* Ensure config directory exists
|
|
51
97
|
*/
|
|
@@ -65,8 +111,15 @@ app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
|
65
111
|
// Mount API routes
|
|
66
112
|
app.use('/__shella', windowsRoutes);
|
|
67
113
|
app.use('/__shella/init', initRoutes);
|
|
114
|
+
app.use('/__shella/status', statusRoutes);
|
|
115
|
+
app.use('/__shella', usageRoutes);
|
|
116
|
+
app.use('/__shella/config', configRoutes);
|
|
117
|
+
app.use('/__shella', promptRoutes);
|
|
118
|
+
app.use('/__shella/local-llm', localLlmRoutes);
|
|
68
119
|
app.use('/__model-state', modelStateRoutes);
|
|
69
120
|
app.use('/__logs', logsRoutes);
|
|
121
|
+
// Session routes with chunking - BEFORE proxy catch-all
|
|
122
|
+
app.use('/api/session', sessionRoutes);
|
|
70
123
|
app.use('/api', proxyRoutes);
|
|
71
124
|
// Production: serve static files from dist/web/
|
|
72
125
|
if (isProd) {
|
|
@@ -102,12 +155,17 @@ async function start() {
|
|
|
102
155
|
}
|
|
103
156
|
// Initialize database before starting server
|
|
104
157
|
await getDatabase();
|
|
105
|
-
//
|
|
158
|
+
// Ensure log directory exists
|
|
106
159
|
const logDir = path.dirname(LOG_FILE);
|
|
107
160
|
if (!fs.existsSync(logDir)) {
|
|
108
161
|
fs.mkdirSync(logDir, { recursive: true });
|
|
109
162
|
}
|
|
110
|
-
|
|
163
|
+
// In dev mode, dev.ts orchestrator manages the log file
|
|
164
|
+
// In prod mode, initialize with JSON entry
|
|
165
|
+
if (isProd) {
|
|
166
|
+
const entry = { level: 30, time: Date.now(), source: 'express', msg: 'Server started' };
|
|
167
|
+
fs.writeFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
168
|
+
}
|
|
111
169
|
app.listen(PORT, () => {
|
|
112
170
|
console.log(`[shella-server] Running on http://localhost:${PORT}`);
|
|
113
171
|
console.log(`[shella-server] Mode: ${isProd ? 'production' : 'development'}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side OpenCode SDK client
|
|
3
|
+
*
|
|
4
|
+
* Creates a client that points directly to the OpenCode server,
|
|
5
|
+
* bypassing the Express proxy. Used for server-side operations
|
|
6
|
+
* that need to fetch/transform data before returning to the frontend.
|
|
7
|
+
*/
|
|
8
|
+
declare const OPENCODE_URL: string;
|
|
9
|
+
/**
|
|
10
|
+
* Server-side OpenCode SDK client.
|
|
11
|
+
* Points directly to OpenCode server (not through Express proxy).
|
|
12
|
+
*/
|
|
13
|
+
export declare const opencodeClient: import("@opencode-ai/sdk/v2/client").OpencodeClient;
|
|
14
|
+
export { OPENCODE_URL };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side OpenCode SDK client
|
|
3
|
+
*
|
|
4
|
+
* Creates a client that points directly to the OpenCode server,
|
|
5
|
+
* bypassing the Express proxy. Used for server-side operations
|
|
6
|
+
* that need to fetch/transform data before returning to the frontend.
|
|
7
|
+
*/
|
|
8
|
+
import { createOpencodeClient } from '@opencode-ai/sdk/v2/client';
|
|
9
|
+
const OPENCODE_URL = process.env.OPENCODE_URL || 'http://localhost:4096';
|
|
10
|
+
/**
|
|
11
|
+
* Server-side OpenCode SDK client.
|
|
12
|
+
* Points directly to OpenCode server (not through Express proxy).
|
|
13
|
+
*/
|
|
14
|
+
export const opencodeClient = createOpencodeClient({
|
|
15
|
+
baseUrl: OPENCODE_URL,
|
|
16
|
+
});
|
|
17
|
+
export { OPENCODE_URL };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode configuration loader for shella
|
|
3
|
+
*
|
|
4
|
+
* Loads plugin and model config to pass to createOpencodeServer().
|
|
5
|
+
* OpenCode auto-installs plugins from npm on first startup.
|
|
6
|
+
*
|
|
7
|
+
* Future optimization: bundle plugin via file:// protocol to skip npm install.
|
|
8
|
+
* Add plugin to package.json deps, then use `plugin: ['file://${pluginPath}']`.
|
|
9
|
+
*/
|
|
10
|
+
import type { Config } from '@opencode-ai/sdk';
|
|
11
|
+
/**
|
|
12
|
+
* Load OpenCode config with plugin and model definitions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getOpencodeConfig(): Config;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode configuration loader for shella
|
|
3
|
+
*
|
|
4
|
+
* Loads plugin and model config to pass to createOpencodeServer().
|
|
5
|
+
* OpenCode auto-installs plugins from npm on first startup.
|
|
6
|
+
*
|
|
7
|
+
* Future optimization: bundle plugin via file:// protocol to skip npm install.
|
|
8
|
+
* Add plugin to package.json deps, then use `plugin: ['file://${pluginPath}']`.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
let cachedConfig = null;
|
|
15
|
+
/**
|
|
16
|
+
* Load OpenCode config with plugin and model definitions.
|
|
17
|
+
*/
|
|
18
|
+
export function getOpencodeConfig() {
|
|
19
|
+
if (cachedConfig)
|
|
20
|
+
return cachedConfig;
|
|
21
|
+
const configPath = resolve(__dirname, '../../config/openai-codex-models.json');
|
|
22
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
23
|
+
cachedConfig = JSON.parse(raw);
|
|
24
|
+
return cachedConfig;
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shella Config Routes
|
|
3
|
+
*
|
|
4
|
+
* Custom config endpoint that wraps OpenCode's provider list and injects
|
|
5
|
+
* the __auto__ provider with dynamically computed routable models.
|
|
6
|
+
*
|
|
7
|
+
* Routable models are models that:
|
|
8
|
+
* 1. Exist on a provider with usage limit support (anthropic, github-copilot, openai)
|
|
9
|
+
* 2. Can be served by at least one other usage-limit provider
|
|
10
|
+
*/
|
|
11
|
+
declare const router: import("express-serve-static-core").Router;
|
|
12
|
+
export default router;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shella Config Routes
|
|
3
|
+
*
|
|
4
|
+
* Custom config endpoint that wraps OpenCode's provider list and injects
|
|
5
|
+
* the __auto__ provider with dynamically computed routable models.
|
|
6
|
+
*
|
|
7
|
+
* Routable models are models that:
|
|
8
|
+
* 1. Exist on a provider with usage limit support (anthropic, github-copilot, openai)
|
|
9
|
+
* 2. Can be served by at least one other usage-limit provider
|
|
10
|
+
*/
|
|
11
|
+
import { Router } from 'express';
|
|
12
|
+
import { isRoutableModel, getUsage } from '@bytespell/model-provider-usage-limits';
|
|
13
|
+
import { opencodeClient } from '../lib/opencode-client.js';
|
|
14
|
+
/** Providers that support usage limits and can participate in auto-routing */
|
|
15
|
+
const USAGE_LIMIT_PROVIDERS = ['anthropic', 'github-copilot', 'openai'];
|
|
16
|
+
const router = Router();
|
|
17
|
+
/**
|
|
18
|
+
* Filter models to prefer "(latest)" variants and clean up display names.
|
|
19
|
+
*
|
|
20
|
+
* When multiple models share the same base name (name without " (latest)"):
|
|
21
|
+
* - If any has "(latest)" in its name, keep only that one
|
|
22
|
+
* - If none have "(latest)", keep all of them
|
|
23
|
+
*
|
|
24
|
+
* All "(latest)" suffixes are stripped from final display names.
|
|
25
|
+
*/
|
|
26
|
+
function filterLatestModels(models) {
|
|
27
|
+
// Group models by base name (name without " (latest)")
|
|
28
|
+
const byBaseName = new Map();
|
|
29
|
+
for (const [modelId, info] of Object.entries(models)) {
|
|
30
|
+
const baseName = info.name.replace(' (latest)', '');
|
|
31
|
+
if (!byBaseName.has(baseName)) {
|
|
32
|
+
byBaseName.set(baseName, []);
|
|
33
|
+
}
|
|
34
|
+
byBaseName.get(baseName).push([modelId, info]);
|
|
35
|
+
}
|
|
36
|
+
// For each group, keep only the "(latest)" version if multiple exist
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [baseName, entries] of byBaseName) {
|
|
39
|
+
if (entries.length === 1) {
|
|
40
|
+
// Single model - keep it with cleaned name
|
|
41
|
+
const [modelId, info] = entries[0];
|
|
42
|
+
result[modelId] = { ...info, name: baseName };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Multiple models with same base name
|
|
46
|
+
const latestEntry = entries.find(([, info]) => info.name.includes('(latest)'));
|
|
47
|
+
if (latestEntry) {
|
|
48
|
+
// Keep only the "(latest)" variant with cleaned name
|
|
49
|
+
const [modelId, info] = latestEntry;
|
|
50
|
+
result[modelId] = { ...info, name: baseName };
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// No "(latest)" found - keep all with cleaned names
|
|
54
|
+
for (const [modelId, info] of entries) {
|
|
55
|
+
result[modelId] = { ...info, name: baseName };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute routable models from connected providers.
|
|
64
|
+
*
|
|
65
|
+
* A model is routable if:
|
|
66
|
+
* 1. It belongs to a connected provider with usage limit support
|
|
67
|
+
* 2. It can be transformed/served by at least one OTHER usage-limit provider
|
|
68
|
+
*
|
|
69
|
+
* @param providers - All available providers
|
|
70
|
+
* @param connected - IDs of connected providers
|
|
71
|
+
* @returns Map of model ID to model info for routable models
|
|
72
|
+
*/
|
|
73
|
+
function computeRoutableModels(providers, connected) {
|
|
74
|
+
const routableModels = {};
|
|
75
|
+
// Get connected providers that support usage limits
|
|
76
|
+
const usageProviders = connected.filter((id) => USAGE_LIMIT_PROVIDERS.includes(id));
|
|
77
|
+
// Need at least 2 usage-limit providers for routing to be meaningful
|
|
78
|
+
if (usageProviders.length < 2) {
|
|
79
|
+
console.log(`[config] Only ${usageProviders.length} usage-limit provider(s) connected, no routable models`);
|
|
80
|
+
return routableModels;
|
|
81
|
+
}
|
|
82
|
+
console.log(`[config] Computing routable models from providers: ${usageProviders.join(', ')}`);
|
|
83
|
+
// For each model in each usage-limit provider, check if it's routable
|
|
84
|
+
for (const providerId of usageProviders) {
|
|
85
|
+
const provider = providers.find((p) => p.id === providerId);
|
|
86
|
+
if (!provider?.models)
|
|
87
|
+
continue;
|
|
88
|
+
for (const [modelId, modelInfo] of Object.entries(provider.models)) {
|
|
89
|
+
// Skip if already added (may exist on multiple providers)
|
|
90
|
+
if (routableModels[modelId])
|
|
91
|
+
continue;
|
|
92
|
+
// Check if this model can be served by at least one OTHER provider
|
|
93
|
+
if (isRoutableModel(modelId, providerId)) {
|
|
94
|
+
routableModels[modelId] = {
|
|
95
|
+
name: modelInfo.name,
|
|
96
|
+
limit: modelInfo.limit,
|
|
97
|
+
cost: modelInfo.cost,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Filter to prefer "(latest)" variants and clean display names
|
|
103
|
+
const filteredModels = filterLatestModels(routableModels);
|
|
104
|
+
const beforeCount = Object.keys(routableModels).length;
|
|
105
|
+
const afterCount = Object.keys(filteredModels).length;
|
|
106
|
+
console.log(`[config] Found ${beforeCount} routable model(s), filtered to ${afterCount}`);
|
|
107
|
+
return filteredModels;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* GET /
|
|
111
|
+
*
|
|
112
|
+
* Returns OpenCode's provider list with __auto__ provider injected.
|
|
113
|
+
* The __auto__ provider contains all models that can be auto-routed
|
|
114
|
+
* between providers based on usage limits.
|
|
115
|
+
*/
|
|
116
|
+
router.get('/', async (_req, res) => {
|
|
117
|
+
console.log('[config] GET /__shella/config');
|
|
118
|
+
try {
|
|
119
|
+
// Fetch real provider list from OpenCode using SDK
|
|
120
|
+
const providersRes = await opencodeClient.provider.list();
|
|
121
|
+
if (providersRes.error) {
|
|
122
|
+
throw new Error(`OpenCode SDK error: ${JSON.stringify(providersRes.error)}`);
|
|
123
|
+
}
|
|
124
|
+
const providers = (providersRes.data?.all ?? []);
|
|
125
|
+
const connected = providersRes.data?.connected ?? [];
|
|
126
|
+
console.log(`[config] OpenCode has ${providers.length} providers, ${connected.length} connected`);
|
|
127
|
+
// Fetch usage data for providers with usage limits (non-blocking on failure)
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
let usageData = {};
|
|
130
|
+
try {
|
|
131
|
+
usageData = await getUsage({ autoDetectAuthTokens: true });
|
|
132
|
+
console.log(`[config] Fetched usage for ${Object.keys(usageData).length} provider(s)`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
136
|
+
console.error('[config] Failed to fetch usage (non-fatal):', message);
|
|
137
|
+
}
|
|
138
|
+
// Normalize window labels (e.g., "monthly" -> "30d")
|
|
139
|
+
const normalizeWindowLabel = (window) => {
|
|
140
|
+
if (window === 'monthly')
|
|
141
|
+
return '30d';
|
|
142
|
+
return window;
|
|
143
|
+
};
|
|
144
|
+
// Augment providers with usage data (both primary and secondary windows)
|
|
145
|
+
const providersWithUsage = providers.map((provider) => {
|
|
146
|
+
const usage = usageData[provider.id];
|
|
147
|
+
const primary = usage?.data?.primary;
|
|
148
|
+
const secondary = usage?.data?.secondary;
|
|
149
|
+
// Build array of usage windows
|
|
150
|
+
const windows = [];
|
|
151
|
+
if (primary) {
|
|
152
|
+
windows.push({
|
|
153
|
+
usedPercent: primary.usedPercent,
|
|
154
|
+
window: normalizeWindowLabel(primary.window),
|
|
155
|
+
resetsAt: primary.resetsAt,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (secondary) {
|
|
159
|
+
windows.push({
|
|
160
|
+
usedPercent: secondary.usedPercent,
|
|
161
|
+
window: normalizeWindowLabel(secondary.window),
|
|
162
|
+
resetsAt: secondary.resetsAt,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...provider,
|
|
167
|
+
usage: windows.length > 0 ? windows : null,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
// Filter connected providers to only show those actually authenticated
|
|
171
|
+
const actuallyConnected = connected.filter((id) => {
|
|
172
|
+
// Special case: opencode always connected (free models)
|
|
173
|
+
if (id === 'opencode')
|
|
174
|
+
return true;
|
|
175
|
+
// Special case: OpenAI - only show as connected if we got usage data
|
|
176
|
+
// This handles the case where OpenAI has models in config (via opencode-openai-codex-auth
|
|
177
|
+
// plugin) but user hasn't completed OAuth yet. If usage data exists, auth is working.
|
|
178
|
+
// TODO: If the plugin changes how it loads models, this check may need updating
|
|
179
|
+
if (id === 'openai')
|
|
180
|
+
return usageData['openai'] !== undefined;
|
|
181
|
+
// All other providers: trust OpenCode's connected array
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
console.log(`[config] Filtered connected: ${connected.length} -> ${actuallyConnected.length} (${actuallyConnected.join(', ')})`);
|
|
185
|
+
// Compute routable models using actually connected providers
|
|
186
|
+
const routableModels = computeRoutableModels(providers, actuallyConnected);
|
|
187
|
+
// Create __auto__ provider (always included, even if empty)
|
|
188
|
+
const autoProvider = {
|
|
189
|
+
id: '__auto__',
|
|
190
|
+
name: 'Auto Route',
|
|
191
|
+
models: routableModels,
|
|
192
|
+
usage: null, // __auto__ doesn't have its own usage
|
|
193
|
+
};
|
|
194
|
+
// Return combined result with __auto__ first
|
|
195
|
+
// __auto__ is always "connected" so it appears in the UI
|
|
196
|
+
res.json({
|
|
197
|
+
all: [autoProvider, ...providersWithUsage],
|
|
198
|
+
connected: ['__auto__', ...actuallyConnected],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
console.error('[config] Error fetching config:', message);
|
|
204
|
+
res.status(500).json({ error: message });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
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;
|