@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.
Files changed (91) hide show
  1. package/README.md +49 -1
  2. package/bin/cli.js +184 -87
  3. package/dev/cli.tsx +38 -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 +67 -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/directory.d.ts +8 -0
  14. package/dist/server/routes/directory.js +84 -0
  15. package/dist/server/routes/init.d.ts +4 -3
  16. package/dist/server/routes/init.js +23 -9
  17. package/dist/server/routes/local-llm.d.ts +8 -0
  18. package/dist/server/routes/local-llm.js +255 -0
  19. package/dist/server/routes/logs.js +35 -11
  20. package/dist/server/routes/prompt.d.ts +16 -0
  21. package/dist/server/routes/prompt.js +173 -0
  22. package/dist/server/routes/session.d.ts +8 -0
  23. package/dist/server/routes/session.js +63 -0
  24. package/dist/server/routes/status.d.ts +9 -0
  25. package/dist/server/routes/status.js +54 -0
  26. package/dist/server/routes/usage.d.ts +12 -0
  27. package/dist/server/routes/usage.js +60 -0
  28. package/dist/server/routes/windows.js +4 -4
  29. package/dist/server/schema.d.ts +47 -16
  30. package/dist/server/schema.js +8 -1
  31. package/dist/server/services/database.d.ts +10 -1
  32. package/dist/server/services/database.js +19 -6
  33. package/dist/web/assets/{_baseUniq-BXqY9Mam.js → _baseUniq-BxVG561Z.js} +1 -1
  34. package/dist/web/assets/{arc-Bn6tUpO_.js → arc-B9TFF79T.js} +1 -1
  35. package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BMRLMpf8.js} +1 -1
  36. package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-DBQKFxeQ.js} +1 -1
  37. package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-TiYEZrdu.js} +1 -1
  38. package/dist/web/assets/channel-aOIIaiSs.js +1 -0
  39. package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-CWCp0N17.js} +1 -1
  40. package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CeVCFKqv.js} +1 -1
  41. package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-B-AoaHJt.js} +1 -1
  42. package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-CCGkXnX-.js} +1 -1
  43. package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-Bcupjeb_.js} +1 -1
  44. package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-DlyUQaTO.js} +1 -1
  45. package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-THN-at_3.js} +1 -1
  46. package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-CtErOFJM.js} +1 -1
  47. package/dist/web/assets/classDiagram-2ON5EDUG-BFLMv18M.js +1 -0
  48. package/dist/web/assets/classDiagram-v2-WZHVMYZB-BFLMv18M.js +1 -0
  49. package/dist/web/assets/clone-BR_FHSwu.js +1 -0
  50. package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-CPI-88R6.js} +1 -1
  51. package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-YrHmsLe4.js} +1 -1
  52. package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-Dcvw3qhj.js} +1 -1
  53. package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-B135EOe6.js} +1 -1
  54. package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-w3KdB_-u.js} +1 -1
  55. package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DYssvOTP.js} +1 -1
  56. package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-DpnuE7B_.js} +1 -1
  57. package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-BhcJ-8Yu.js} +1 -1
  58. package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-ButVkRCz.js} +1 -1
  59. package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-ZLz8eoSo.js} +1 -1
  60. package/dist/web/assets/{graph-DqhaNOTU.js → graph-CKmCFGqF.js} +1 -1
  61. package/dist/web/assets/index-BHJDUcNL.js +1719 -0
  62. package/dist/web/assets/index-CcAJUkQw.css +1 -0
  63. package/dist/web/assets/index-DEiKajXR.js +1 -0
  64. package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-C_h94brE.js} +1 -1
  65. package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-u0bPRxxb.js} +1 -1
  66. package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-DkM-KD6Y.js} +1 -1
  67. package/dist/web/assets/{layout-Cc1ESzTe.js → layout-DGSU3MQw.js} +1 -1
  68. package/dist/web/assets/{linear-BwI2ANFG.js → linear-Dck9QCb9.js} +1 -1
  69. package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-DfB-jqaz.js} +5 -5
  70. package/dist/web/assets/{min--MKscDc6.js → min-CqEcl9J0.js} +1 -1
  71. package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-D1KrSALz.js} +1 -1
  72. package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-CZ-507Bd.js} +1 -1
  73. package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-Jw0og6Ix.js} +1 -1
  74. package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-5JWD7TEH.js} +1 -1
  75. package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-DzlxPj37.js} +1 -1
  76. package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-Cui1ykiA.js} +1 -1
  77. package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-CCdGE_zt.js} +1 -1
  78. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CsLg9bzy.js +1 -0
  79. package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-CP2T8mHI.js} +1 -1
  80. package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-DGBVlHVf.js} +1 -1
  81. package/dist/web/assets/welcome-screen-test-DnIwI3hf.js +1 -0
  82. package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-DDlMipkA.js} +1 -1
  83. package/dist/web/index.html +3 -3
  84. package/package.json +14 -5
  85. package/dist/web/assets/channel-CxjnQtV7.js +0 -1
  86. package/dist/web/assets/classDiagram-2ON5EDUG-CVG91-fs.js +0 -1
  87. package/dist/web/assets/classDiagram-v2-WZHVMYZB-CVG91-fs.js +0 -1
  88. package/dist/web/assets/clone-C7jxvixc.js +0 -1
  89. package/dist/web/assets/index-B0jWvqrS.css +0 -1
  90. package/dist/web/assets/index-Dnmavb3d.js +0 -1716
  91. 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
- * 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;
@@ -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
- // POST /__logs - receive browser console logs
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 prefix = level === 'error' ? '🔴' : level === 'warn' ? '🟡' : '🌐';
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
- const line = `${timestamp} ${prefix} ${message}`;
16
- // Log to terminal
17
- console.log(line);
18
- // Append to file
19
- fs.appendFileSync(LOG_FILE, line + '\n');
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 fallback = `${new Date().toISOString().slice(11, 23)} 🌐 [parse error] ${JSON.stringify(req.body)}`;
24
- console.log(fallback);
25
- fs.appendFileSync(LOG_FILE, fallback + '\n');
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;