@openchamber/web 1.11.5 → 1.11.7

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 (65) hide show
  1. package/README.md +6 -0
  2. package/bin/cli.js +443 -2
  3. package/dist/assets/{MarkdownRendererImpl-C3-ZpwEx.js → MarkdownRendererImpl-DaF15QNC.js} +1 -1
  4. package/dist/assets/{MultiRunWindow-BDfPzMDy.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
  5. package/dist/assets/{OnboardingScreen-DGgh4IXB.js → OnboardingScreen-DTv6YJI1.js} +2 -2
  6. package/dist/assets/{SettingsWindow-B8QKr5dB.js → SettingsWindow-_c3TTL2z.js} +1 -1
  7. package/dist/assets/{TerminalView-D7IIkSGJ.js → TerminalView-CuXkDROt.js} +4 -4
  8. package/dist/assets/es-CYoUf2D-.js +15 -0
  9. package/dist/assets/{index-DHluop4D.js → index-3WXrN3AX.js} +1 -1
  10. package/dist/assets/index-BREIbhcb.css +1 -0
  11. package/dist/assets/ko-2tM0fIna.js +15 -0
  12. package/dist/assets/main-BF3kWAJ9.js +239 -0
  13. package/dist/assets/{main-VVcyjpiF.js → main-o8ZERrmU.js} +2 -2
  14. package/dist/assets/miniChat-BZQjpK23.js +2 -0
  15. package/dist/assets/{modelPrefsAutoSave-Ctdc3cCY.js → modelPrefsAutoSave-wwnbqBk7.js} +109 -107
  16. package/dist/assets/pl-Dq8uAotM.js +15 -0
  17. package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
  18. package/dist/assets/{renderElectronMiniChatApp-CsddCM3q.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
  19. package/dist/assets/uk-BZtz0wUV.js +15 -0
  20. package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
  21. package/dist/assets/zh-CN-j_nYMchE.js +15 -0
  22. package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
  23. package/dist/index.html +11 -28
  24. package/dist/mini-chat.html +4 -4
  25. package/package.json +1 -1
  26. package/server/index.js +2 -0
  27. package/server/lib/cloudflare-tunnel.js +3 -5
  28. package/server/lib/fs/routes.js +5 -0
  29. package/server/lib/fs/routes.test.js +61 -1
  30. package/server/lib/git/DOCUMENTATION.md +1 -0
  31. package/server/lib/git/routes.js +82 -1
  32. package/server/lib/git/service.js +338 -19
  33. package/server/lib/git/service.test.js +414 -8
  34. package/server/lib/ngrok-tunnel.js +209 -0
  35. package/server/lib/opencode/core-routes.js +1 -0
  36. package/server/lib/opencode/env-runtime.js +52 -4
  37. package/server/lib/opencode/env-runtime.test.js +82 -6
  38. package/server/lib/opencode/feature-routes-runtime.js +35 -0
  39. package/server/lib/opencode/index.js +19 -0
  40. package/server/lib/opencode/npm-registry.js +157 -0
  41. package/server/lib/opencode/npm-registry.test.js +179 -0
  42. package/server/lib/opencode/openchamber-routes.js +9 -7
  43. package/server/lib/opencode/plugin-routes.js +373 -0
  44. package/server/lib/opencode/plugin-routes.test.js +384 -0
  45. package/server/lib/opencode/plugin-spec.js +107 -0
  46. package/server/lib/opencode/plugin-spec.test.js +154 -0
  47. package/server/lib/opencode/plugins.js +393 -0
  48. package/server/lib/opencode/plugins.test.js +176 -0
  49. package/server/lib/opencode/settings-helpers.js +6 -0
  50. package/server/lib/opencode/settings-helpers.test.js +11 -0
  51. package/server/lib/opencode/settings-runtime.js +39 -1
  52. package/server/lib/opencode/settings-runtime.test.js +39 -0
  53. package/server/lib/skills-catalog/source.js +1 -1
  54. package/server/lib/tunnels/DOCUMENTATION.md +1 -0
  55. package/server/lib/tunnels/providers/ngrok.js +117 -0
  56. package/server/lib/tunnels/types.js +2 -0
  57. package/dist/assets/es-dIVpApmS.js +0 -15
  58. package/dist/assets/index-Bk9IWJe1.css +0 -1
  59. package/dist/assets/ko-Cqf3E9-d.js +0 -15
  60. package/dist/assets/main-D45l3Dxw.js +0 -232
  61. package/dist/assets/miniChat-a9w7WM0c.js +0 -2
  62. package/dist/assets/pl-C577DpsX.js +0 -15
  63. package/dist/assets/pt-BR-BeeF6VlK.js +0 -15
  64. package/dist/assets/uk-CZ7XVz_D.js +0 -15
  65. package/dist/assets/zh-CN-BMSSqdyO.js +0 -15
@@ -1,7 +1,6 @@
1
1
  export const registerOpenChamberRoutes = (app, dependencies) => {
2
2
  const {
3
3
  fs,
4
- os,
5
4
  path,
6
5
  process,
7
6
  server,
@@ -104,14 +103,15 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
104
103
  }
105
104
 
106
105
  const currentPort = server.address()?.port || 3000;
107
- const tmpDir = os.tmpdir();
108
- const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`);
106
+ const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
109
107
  let storedOptions = { port: currentPort, daemon: true };
110
108
  try {
111
109
  const content = await fs.promises.readFile(instanceFilePath, 'utf8');
112
110
  storedOptions = JSON.parse(content);
113
111
  } catch {
114
112
  }
113
+ const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
114
+ const isForegroundService = launchMode === 'foreground';
115
115
 
116
116
  const isWindows = process.platform === 'win32';
117
117
  const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
@@ -152,7 +152,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
152
152
  restartCmdFallback += ` --ui-password '${escapedPw}'`;
153
153
  }
154
154
  }
155
- const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
155
+ const restartCmd = isForegroundService ? '' : `(${restartCmdPrimary}) || (${restartCmdFallback})`;
156
156
  const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
157
157
  const logPreamble = [
158
158
  '',
@@ -165,8 +165,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
165
165
  `packagePath=${pmDetails.packagePath || 'unknown'}`,
166
166
  `globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
167
167
  `mode=${isContainer ? 'container' : 'restart'}`,
168
+ `launchMode=${launchMode}`,
168
169
  `updateCommand=${updateCmd}`,
169
- `restartCommand=${restartCmd}`,
170
+ `restartCommand=${restartCmd || 'service-manager'}`,
170
171
  `logPath=${updateLogPath}`,
171
172
  ].join('\n');
172
173
 
@@ -176,6 +177,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
176
177
  version: updateInfo.version,
177
178
  packageManager: pm,
178
179
  autoRestart: true,
180
+ restartManager: isForegroundService ? 'service' : 'cli',
179
181
  });
180
182
 
181
183
  setTimeout(() => {
@@ -192,7 +194,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
192
194
  ${updateCmd}
193
195
  if %ERRORLEVEL% EQU 0 (
194
196
  echo Update successful, restarting OpenChamber...
195
- ${restartCmd}
197
+ ${restartCmd || 'echo Service manager will restart OpenChamber.'}
196
198
  ) else (
197
199
  echo Update failed
198
200
  exit /b 1
@@ -204,7 +206,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
204
206
  ${updateCmd}
205
207
  if [ $? -eq 0 ]; then
206
208
  echo "Update successful, restarting OpenChamber..."
207
- ${restartCmd}
209
+ ${restartCmd || 'echo "Service manager will restart OpenChamber."'}
208
210
  else
209
211
  echo "Update failed"
210
212
  exit 1
@@ -0,0 +1,373 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+
4
+ import { getNpmInfo as defaultGetNpmInfo } from './npm-registry.js';
5
+ import { isExactSemver as defaultIsExactSemver, isPathSpec as defaultIsPathSpec, parseNpmSpec as defaultParseNpmSpec, parsePathSpec as defaultParsePathSpec } from './plugin-spec.js';
6
+
7
+ const ENTRY_EXISTS_CODES = new Set(['ENTRY_EXISTS', 'EEXIST']);
8
+ const FILE_EXISTS_CODES = new Set(['FILE_EXISTS', 'EEXIST']);
9
+ const NOT_FOUND_CODES = new Set(['NOT_FOUND', 'ENOENT']);
10
+ const BAD_REQUEST_CODES = new Set(['INVALID_FILENAME', 'INVALID_SCOPE', 'INVALID_SPEC', 'EINVAL']);
11
+
12
+ export const registerPluginRoutes = (app, dependencies) => {
13
+ const {
14
+ resolveOptionalProjectDirectory,
15
+ refreshOpenCodeAfterConfigChange,
16
+ clientReloadDelayMs,
17
+ listPluginEntries,
18
+ getPluginEntry,
19
+ createPluginEntry,
20
+ updatePluginEntry,
21
+ deletePluginEntry,
22
+ listPluginDirFiles,
23
+ readPluginDirFile,
24
+ writePluginDirFile,
25
+ deletePluginDirFile,
26
+ encodePluginId,
27
+ decodePluginId,
28
+ getNpmInfo = defaultGetNpmInfo,
29
+ parseNpmSpec = defaultParseNpmSpec,
30
+ parsePathSpec = defaultParsePathSpec,
31
+ isExactSemver = defaultIsExactSemver,
32
+ isPathSpec = defaultIsPathSpec,
33
+ } = dependencies;
34
+
35
+ const parsedKindForSpec = (spec) => (isPathSpec(spec) ? 'path' : 'npm');
36
+
37
+ const resolveDirectory = async (req, res) => {
38
+ const { directory, error } = await resolveOptionalProjectDirectory(req);
39
+ if (error) {
40
+ res.status(400).json({ error });
41
+ return null;
42
+ }
43
+ return directory || null;
44
+ };
45
+
46
+ const successPayload = (message) => ({
47
+ success: true,
48
+ requiresReload: true,
49
+ message,
50
+ reloadDelayMs: clientReloadDelayMs,
51
+ reloadFailed: false,
52
+ warning: undefined,
53
+ });
54
+
55
+ const completePluginMutation = async (res, operation, _noun, applyChange) => {
56
+ applyChange();
57
+
58
+ const pastTense = operation.replace(/ion$/, 'ed').replace(/update$/, 'updated');
59
+
60
+ try {
61
+ await refreshOpenCodeAfterConfigChange(`plugin ${operation}`);
62
+ return res.json(successPayload(`Plugin ${pastTense}. Reloading interface…`));
63
+ } catch (error) {
64
+ console.error(`[API:plugin ${operation}] Reload failed after config write:`, error);
65
+ return res.json({
66
+ success: true,
67
+ requiresReload: false,
68
+ message: `Plugin ${pastTense}, but OpenCode reload failed.`,
69
+ reloadDelayMs: clientReloadDelayMs,
70
+ reloadFailed: true,
71
+ warning: error.message || 'OpenCode reload failed after plugin config changed',
72
+ });
73
+ }
74
+ };
75
+
76
+ const validateEntryId = (id) => {
77
+ const decoded = decodePluginId(id);
78
+ if (decoded.prefix !== 'config') {
79
+ const error = new Error('Plugin entry not found');
80
+ error.code = 'NOT_FOUND';
81
+ throw error;
82
+ }
83
+ };
84
+
85
+ const validateFileId = (id) => {
86
+ const decoded = decodePluginId(id);
87
+ if (decoded.prefix !== 'file') {
88
+ const error = new Error('Plugin file not found');
89
+ error.code = 'NOT_FOUND';
90
+ throw error;
91
+ }
92
+ };
93
+
94
+ const handlePluginError = (res, error, fallbackMessage, context, existsKind = null) => {
95
+ const code = error?.code;
96
+ if ((existsKind === 'entry' && ENTRY_EXISTS_CODES.has(code)) || (existsKind === 'file' && FILE_EXISTS_CODES.has(code))) {
97
+ return res.status(409).json({ error: error.message });
98
+ }
99
+ if (NOT_FOUND_CODES.has(code)) {
100
+ return res.status(404).json({ error: error.message });
101
+ }
102
+ if (BAD_REQUEST_CODES.has(code)) {
103
+ return res.status(400).json({ error: error.message });
104
+ }
105
+
106
+ console.error(context, error);
107
+ return res.status(500).json({ error: fallbackMessage });
108
+ };
109
+
110
+ app.get('/api/config/plugins', async (req, res) => {
111
+ try {
112
+ const directory = await resolveDirectory(req, res);
113
+ if (directory === null && res.headersSent) return;
114
+
115
+ res.json({
116
+ entries: listPluginEntries(directory),
117
+ files: listPluginDirFiles(directory),
118
+ });
119
+ } catch (error) {
120
+ console.error('[API:GET /api/config/plugins] Failed:', error);
121
+ res.status(500).json({ error: 'Failed to list plugins' });
122
+ }
123
+ });
124
+
125
+ app.get('/api/config/plugins/registry', async (req, res) => {
126
+ try {
127
+ const { directory, error: directoryError } = await resolveOptionalProjectDirectory(req);
128
+ if (directoryError) {
129
+ return res.status(400).json({ error: directoryError });
130
+ }
131
+ const rawSpecs = (req.query.specs || '').toString();
132
+ const specs = rawSpecs
133
+ ? rawSpecs.split(',').map((spec) => {
134
+ try {
135
+ return decodeURIComponent(spec);
136
+ } catch {
137
+ return spec;
138
+ }
139
+ }).filter((spec) => spec.length > 0)
140
+ : [];
141
+ const uniqueSpecs = Array.from(new Set(specs));
142
+ if (uniqueSpecs.length > 100) {
143
+ return res.status(400).json({ error: 'too many specs' });
144
+ }
145
+
146
+ const refresh = req.query.refresh === 'true';
147
+ const npmJobs = new Map();
148
+ const malformedSpecs = new Set();
149
+
150
+ for (const spec of uniqueSpecs) {
151
+ if (parsedKindForSpec(spec) !== 'npm') continue;
152
+
153
+ const parsed = parseNpmSpec(spec);
154
+ if (parsed.malformed) {
155
+ malformedSpecs.add(spec);
156
+ continue;
157
+ }
158
+
159
+ const job = npmJobs.get(parsed.name) || { specs: [], parsedBySpec: new Map() };
160
+ job.specs.push(spec);
161
+ job.parsedBySpec.set(spec, parsed);
162
+ npmJobs.set(parsed.name, job);
163
+ }
164
+
165
+ const npmInfoByName = new Map();
166
+ await Promise.all(Array.from(npmJobs.keys()).map(async (name) => {
167
+ npmInfoByName.set(name, await getNpmInfo(name, { forceRefresh: refresh }));
168
+ }));
169
+
170
+ const results = [];
171
+ for (const spec of uniqueSpecs) {
172
+ if (malformedSpecs.has(spec)) {
173
+ results.push({ kind: 'npm-malformed', spec, error: 'Spec syntax is malformed' });
174
+ continue;
175
+ }
176
+
177
+ if (parsedKindForSpec(spec) === 'path') {
178
+ const { absolutePath } = parsePathSpec(spec, { homedir: os.homedir(), cwd: directory || os.homedir() });
179
+ try {
180
+ fs.statSync(absolutePath);
181
+ } catch {
182
+ results.push({ kind: 'path-missing', spec, absolutePath });
183
+ continue;
184
+ }
185
+
186
+ try {
187
+ fs.accessSync(absolutePath, fs.constants.R_OK);
188
+ results.push({ kind: 'path-ok', spec, absolutePath });
189
+ } catch {
190
+ results.push({ kind: 'path-unreadable', spec, absolutePath });
191
+ }
192
+ continue;
193
+ }
194
+
195
+ const parsed = parseNpmSpec(spec);
196
+ const info = npmInfoByName.get(parsed.name);
197
+ if (!info.ok) {
198
+ if (info.status === 404) {
199
+ results.push({ kind: 'npm-missing-package', spec, name: parsed.name, error: info.error });
200
+ continue;
201
+ }
202
+
203
+ results.push({ kind: 'npm-network', spec, error: info.status === 'network' ? info.error : `Registry returned ${info.status}` });
204
+ continue;
205
+ }
206
+
207
+ const currentVersion = parsed.version;
208
+ if (currentVersion !== null && isExactSemver(currentVersion) && !info.versions.includes(currentVersion)) {
209
+ results.push({
210
+ kind: 'npm-missing-version',
211
+ spec,
212
+ name: parsed.name,
213
+ currentVersion,
214
+ latestVersion: info.latest,
215
+ versions: info.versions,
216
+ });
217
+ continue;
218
+ }
219
+
220
+ results.push({
221
+ kind: 'npm-ok',
222
+ spec,
223
+ name: parsed.name,
224
+ currentVersion,
225
+ latestVersion: info.latest,
226
+ versions: info.versions,
227
+ hasUpdate: currentVersion !== null && isExactSemver(currentVersion) && currentVersion !== info.latest,
228
+ });
229
+ }
230
+
231
+ return res.json({ results });
232
+ } catch (error) {
233
+ console.error('[API:GET /api/config/plugins/registry]', error);
234
+ return res.status(500).json({ error: 'Failed to query npm registry' });
235
+ }
236
+ });
237
+
238
+ app.get('/api/config/plugins/entry/:id', async (req, res) => {
239
+ try {
240
+ const directory = await resolveDirectory(req, res);
241
+ if (directory === null && res.headersSent) return;
242
+ validateEntryId(req.params.id);
243
+
244
+ const entry = getPluginEntry(req.params.id, directory);
245
+ if (!entry) {
246
+ return res.status(404).json({ error: 'Plugin entry not found' });
247
+ }
248
+ return res.json(entry);
249
+ } catch (error) {
250
+ return handlePluginError(res, error, 'Failed to get plugin entry', '[API:GET /api/config/plugins/entry/:id] Failed:');
251
+ }
252
+ });
253
+
254
+ app.post('/api/config/plugins/entry', async (req, res) => {
255
+ try {
256
+ const directory = await resolveDirectory(req, res);
257
+ if (directory === null && res.headersSent) return;
258
+
259
+ await completePluginMutation(res, 'entry creation', 'entry', () => {
260
+ createPluginEntry({
261
+ spec: req.body?.spec,
262
+ options: req.body?.options,
263
+ scope: req.body?.scope,
264
+ }, directory);
265
+ });
266
+ } catch (error) {
267
+ return handlePluginError(res, error, 'Failed to create plugin entry', '[API:POST /api/config/plugins/entry] Failed:', 'entry');
268
+ }
269
+ });
270
+
271
+ app.patch('/api/config/plugins/entry/:id', async (req, res) => {
272
+ try {
273
+ const directory = await resolveDirectory(req, res);
274
+ if (directory === null && res.headersSent) return;
275
+ validateEntryId(req.params.id);
276
+
277
+ await completePluginMutation(res, 'entry update', 'entry', () => {
278
+ updatePluginEntry(req.params.id, {
279
+ spec: req.body?.spec,
280
+ options: req.body?.options,
281
+ }, directory);
282
+ });
283
+ } catch (error) {
284
+ return handlePluginError(res, error, 'Failed to update plugin entry', '[API:PATCH /api/config/plugins/entry/:id] Failed:', 'entry');
285
+ }
286
+ });
287
+
288
+ app.delete('/api/config/plugins/entry/:id', async (req, res) => {
289
+ try {
290
+ const directory = await resolveDirectory(req, res);
291
+ if (directory === null && res.headersSent) return;
292
+ validateEntryId(req.params.id);
293
+
294
+ await completePluginMutation(res, 'entry deletion', 'entry', () => {
295
+ deletePluginEntry(req.params.id, directory);
296
+ });
297
+ } catch (error) {
298
+ return handlePluginError(res, error, 'Failed to delete plugin entry', '[API:DELETE /api/config/plugins/entry/:id] Failed:', 'entry');
299
+ }
300
+ });
301
+
302
+ app.get('/api/config/plugins/file/:id', async (req, res) => {
303
+ try {
304
+ const directory = await resolveDirectory(req, res);
305
+ if (directory === null && res.headersSent) return;
306
+ validateFileId(req.params.id);
307
+
308
+ const file = readPluginDirFile(req.params.id, directory);
309
+ if (!file) {
310
+ return res.status(404).json({ error: 'Plugin file not found' });
311
+ }
312
+ return res.json(file);
313
+ } catch (error) {
314
+ return handlePluginError(res, error, 'Failed to read plugin file', '[API:GET /api/config/plugins/file/:id] Failed:');
315
+ }
316
+ });
317
+
318
+ app.post('/api/config/plugins/file', async (req, res) => {
319
+ try {
320
+ const directory = await resolveDirectory(req, res);
321
+ if (directory === null && res.headersSent) return;
322
+ const id = encodePluginId('file', `${req.body?.scope || 'user'}:${req.body?.fileName || ''}`);
323
+
324
+ await completePluginMutation(res, 'file creation', 'file', () => {
325
+ validateFileId(id);
326
+ writePluginDirFile({
327
+ fileName: req.body?.fileName,
328
+ content: req.body?.content,
329
+ scope: req.body?.scope,
330
+ }, directory);
331
+ });
332
+ } catch (error) {
333
+ return handlePluginError(res, error, 'Failed to create plugin file', '[API:POST /api/config/plugins/file] Failed:', 'file');
334
+ }
335
+ });
336
+
337
+ app.put('/api/config/plugins/file/:id', async (req, res) => {
338
+ try {
339
+ const directory = await resolveDirectory(req, res);
340
+ if (directory === null && res.headersSent) return;
341
+ validateFileId(req.params.id);
342
+
343
+ const existing = readPluginDirFile(req.params.id, directory);
344
+ if (!existing) {
345
+ return res.status(404).json({ error: 'Plugin file not found' });
346
+ }
347
+
348
+ await completePluginMutation(res, 'file update', 'file', () => {
349
+ writePluginDirFile({
350
+ fileName: existing.fileName,
351
+ content: req.body?.content,
352
+ scope: existing.scope,
353
+ }, directory, { overwrite: true });
354
+ });
355
+ } catch (error) {
356
+ return handlePluginError(res, error, 'Failed to update plugin file', '[API:PUT /api/config/plugins/file/:id] Failed:', 'file');
357
+ }
358
+ });
359
+
360
+ app.delete('/api/config/plugins/file/:id', async (req, res) => {
361
+ try {
362
+ const directory = await resolveDirectory(req, res);
363
+ if (directory === null && res.headersSent) return;
364
+ validateFileId(req.params.id);
365
+
366
+ await completePluginMutation(res, 'file deletion', 'file', () => {
367
+ deletePluginDirFile(req.params.id, directory);
368
+ });
369
+ } catch (error) {
370
+ return handlePluginError(res, error, 'Failed to delete plugin file', '[API:DELETE /api/config/plugins/file/:id] Failed:', 'file');
371
+ }
372
+ });
373
+ };