@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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +153 -82
  3. package/dev/cli.tsx +26 -0
  4. package/dist/config/openai-codex-models.json +205 -0
  5. package/dist/server/index.d.ts +4 -2
  6. package/dist/server/index.js +66 -8
  7. package/dist/server/lib/opencode-client.d.ts +14 -0
  8. package/dist/server/lib/opencode-client.js +17 -0
  9. package/dist/server/lib/opencode-config.d.ts +14 -0
  10. package/dist/server/lib/opencode-config.js +25 -0
  11. package/dist/server/routes/config.d.ts +12 -0
  12. package/dist/server/routes/config.js +207 -0
  13. package/dist/server/routes/init.d.ts +4 -3
  14. package/dist/server/routes/init.js +23 -9
  15. package/dist/server/routes/local-llm.d.ts +8 -0
  16. package/dist/server/routes/local-llm.js +255 -0
  17. package/dist/server/routes/logs.js +35 -11
  18. package/dist/server/routes/prompt.d.ts +16 -0
  19. package/dist/server/routes/prompt.js +173 -0
  20. package/dist/server/routes/session.d.ts +8 -0
  21. package/dist/server/routes/session.js +63 -0
  22. package/dist/server/routes/status.d.ts +9 -0
  23. package/dist/server/routes/status.js +54 -0
  24. package/dist/server/routes/usage.d.ts +12 -0
  25. package/dist/server/routes/usage.js +60 -0
  26. package/dist/server/routes/windows.js +4 -4
  27. package/dist/server/schema.d.ts +47 -16
  28. package/dist/server/schema.js +8 -1
  29. package/dist/server/services/database.d.ts +10 -1
  30. package/dist/server/services/database.js +19 -6
  31. package/dist/web/assets/{_baseUniq-BXqY9Mam.js → _baseUniq-6T01QAux.js} +1 -1
  32. package/dist/web/assets/{arc-Bn6tUpO_.js → arc-BkH3TPJb.js} +1 -1
  33. package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BSi6BLCC.js} +1 -1
  34. package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-QSPUbinO.js} +1 -1
  35. package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-Cya_BihR.js} +1 -1
  36. package/dist/web/assets/channel-DGAtS-pa.js +1 -0
  37. package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-DIL6eizv.js} +1 -1
  38. package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CgwejoZz.js} +1 -1
  39. package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-9mIPqoGe.js} +1 -1
  40. package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-BRbyRfgT.js} +1 -1
  41. package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-CVBT25Fj.js} +1 -1
  42. package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-rTj-WT2G.js} +1 -1
  43. package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-BaUBiHya.js} +1 -1
  44. package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-C4_O5TI-.js} +1 -1
  45. package/dist/web/assets/classDiagram-2ON5EDUG-DLvlUUJq.js +1 -0
  46. package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLvlUUJq.js +1 -0
  47. package/dist/web/assets/clone-BZW2JABw.js +1 -0
  48. package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-Bj_2OIYt.js} +1 -1
  49. package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-T7a1luWi.js} +1 -1
  50. package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-CeH5ZsdW.js} +1 -1
  51. package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-Cdod2Lna.js} +1 -1
  52. package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-CYks2r54.js} +1 -1
  53. package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DCmy0g7p.js} +1 -1
  54. package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-Dlz1bNvI.js} +1 -1
  55. package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-Di5Iit1B.js} +1 -1
  56. package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-9i1dugg-.js} +1 -1
  57. package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-BORbMVri.js} +1 -1
  58. package/dist/web/assets/{graph-DqhaNOTU.js → graph-C0SCKxbQ.js} +1 -1
  59. package/dist/web/assets/index-CYVJT8rN.js +1 -0
  60. package/dist/web/assets/index-CcAJUkQw.css +1 -0
  61. package/dist/web/assets/index-CcDdxbB-.js +1719 -0
  62. package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-7ohMQFLY.js} +1 -1
  63. package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-DZp7Z7wE.js} +1 -1
  64. package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-BCNLCm54.js} +1 -1
  65. package/dist/web/assets/{layout-Cc1ESzTe.js → layout-AUnZuY21.js} +1 -1
  66. package/dist/web/assets/{linear-BwI2ANFG.js → linear-B0bfAqGt.js} +1 -1
  67. package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-D5fXNCxA.js} +5 -5
  68. package/dist/web/assets/{min--MKscDc6.js → min-BZUFOEEw.js} +1 -1
  69. package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-hEGJLJ8N.js} +1 -1
  70. package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-BRpCTJIO.js} +1 -1
  71. package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-m7jaiHQb.js} +1 -1
  72. package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-Coh9g9Sp.js} +1 -1
  73. package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-CrD_kUGR.js} +1 -1
  74. package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-C04yD1EI.js} +1 -1
  75. package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-DhP-DMZW.js} +1 -1
  76. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-DWi5vrD6.js +1 -0
  77. package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-40iW2p_5.js} +1 -1
  78. package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-BnxWQbzt.js} +1 -1
  79. package/dist/web/assets/welcome-screen-test-CLeWuIqq.js +1 -0
  80. package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-D6lcJDCc.js} +1 -1
  81. package/dist/web/index.html +3 -3
  82. package/package.json +14 -5
  83. package/dist/web/assets/channel-CxjnQtV7.js +0 -1
  84. package/dist/web/assets/classDiagram-2ON5EDUG-CVG91-fs.js +0 -1
  85. package/dist/web/assets/classDiagram-v2-WZHVMYZB-CVG91-fs.js +0 -1
  86. package/dist/web/assets/clone-C7jxvixc.js +0 -1
  87. package/dist/web/assets/index-B0jWvqrS.css +0 -1
  88. package/dist/web/assets/index-Dnmavb3d.js +0 -1716
  89. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-oHTO1yj_.js +0 -1
@@ -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
- const CONFIG_DIR = path.join(os.homedir(), '.config', 'shella');
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 --projects-dir from CLI args
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
- // Projects directory - passed from CLI or .env.local
46
- export const PROJECTS_DIR = parseProjectsDir();
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.3';
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
- // Clear/create log file on server start
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
- fs.writeFileSync(LOG_FILE, `--- Shella server started ${new Date().toISOString()} ---\n`);
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
- * Shella initialization endpoint
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
- * For now, this just provides the projects directory info that the CLI
12
- * passes to the server, so the frontend knows where Shella was started.
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
- * Shella initialization endpoint
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
- * For now, this just provides the projects directory info that the CLI
12
- * passes to the server, so the frontend knows where Shella was started.
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
- res.json({
20
- projectsDir: PROJECTS_DIR,
21
- projectsDirName: path.basename(PROJECTS_DIR),
22
- version: VERSION,
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;