@nuraly/lumenjs 0.1.4 → 0.3.0

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 (96) hide show
  1. package/README.md +48 -7
  2. package/dist/auth/native-auth.d.ts +9 -0
  3. package/dist/auth/native-auth.js +49 -2
  4. package/dist/auth/routes/login.js +24 -1
  5. package/dist/auth/routes/totp.d.ts +22 -0
  6. package/dist/auth/routes/totp.js +232 -0
  7. package/dist/auth/routes.js +14 -0
  8. package/dist/auth/token.js +2 -2
  9. package/dist/build/build-markdown.d.ts +15 -0
  10. package/dist/build/build-markdown.js +90 -0
  11. package/dist/build/build-server.d.ts +2 -1
  12. package/dist/build/build-server.js +12 -4
  13. package/dist/build/build.js +46 -5
  14. package/dist/build/scan.d.ts +1 -0
  15. package/dist/build/scan.js +2 -1
  16. package/dist/build/serve-static.js +2 -1
  17. package/dist/build/serve.js +131 -11
  18. package/dist/dev-server/config.js +18 -1
  19. package/dist/dev-server/index-html.d.ts +1 -0
  20. package/dist/dev-server/index-html.js +4 -1
  21. package/dist/dev-server/plugins/vite-plugin-llms.js +1 -0
  22. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +4 -3
  23. package/dist/dev-server/plugins/vite-plugin-loaders.js +4 -3
  24. package/dist/dev-server/plugins/vite-plugin-routes.js +3 -2
  25. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +34 -6
  26. package/dist/dev-server/server.js +146 -88
  27. package/dist/dev-server/ssr-render.js +10 -2
  28. package/dist/editor/ai/backend.js +11 -2
  29. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  30. package/dist/editor/ai/deepseek-client.js +113 -0
  31. package/dist/editor/ai/opencode-client.d.ts +1 -1
  32. package/dist/editor/ai/opencode-client.js +21 -47
  33. package/dist/editor/ai/types.d.ts +1 -1
  34. package/dist/editor/ai/types.js +2 -2
  35. package/dist/editor/ai-chat-panel.js +27 -1
  36. package/dist/editor/editor-bridge.js +2 -1
  37. package/dist/editor/overlay-hmr.js +2 -1
  38. package/dist/llms/generate.d.ts +15 -1
  39. package/dist/llms/generate.js +54 -44
  40. package/dist/runtime/app-shell.d.ts +1 -1
  41. package/dist/runtime/app-shell.js +1 -0
  42. package/dist/runtime/communication.d.ts +65 -36
  43. package/dist/runtime/communication.js +117 -57
  44. package/dist/runtime/island.d.ts +16 -0
  45. package/dist/runtime/island.js +80 -0
  46. package/dist/runtime/router-hydration.js +9 -2
  47. package/dist/runtime/router.d.ts +3 -1
  48. package/dist/runtime/router.js +51 -3
  49. package/dist/runtime/webrtc.d.ts +44 -0
  50. package/dist/runtime/webrtc.js +263 -13
  51. package/dist/shared/dom-shims.js +4 -2
  52. package/dist/shared/html-to-markdown.d.ts +6 -0
  53. package/dist/shared/html-to-markdown.js +73 -0
  54. package/dist/shared/types.d.ts +1 -0
  55. package/dist/storage/adapters/s3.js +6 -3
  56. package/package.json +33 -7
  57. package/templates/blog/pages/index.ts +3 -3
  58. package/templates/blog/pages/posts/[slug].ts +17 -6
  59. package/templates/blog/pages/tag/[tag].ts +6 -6
  60. package/templates/dashboard/pages/index.ts +7 -7
  61. package/templates/default/pages/index.ts +3 -3
  62. package/templates/social/api/posts/[id].ts +0 -14
  63. package/templates/social/api/posts.ts +0 -11
  64. package/templates/social/api/profile/[username].ts +0 -10
  65. package/templates/social/api/upload.ts +0 -19
  66. package/templates/social/data/migrations/001_init.sql +0 -78
  67. package/templates/social/data/migrations/002_add_image_url.sql +0 -1
  68. package/templates/social/data/migrations/003_auth.sql +0 -7
  69. package/templates/social/docs/architecture.md +0 -76
  70. package/templates/social/docs/components.md +0 -100
  71. package/templates/social/docs/data.md +0 -89
  72. package/templates/social/docs/pages.md +0 -96
  73. package/templates/social/docs/theming.md +0 -52
  74. package/templates/social/lib/media.ts +0 -130
  75. package/templates/social/lumenjs.auth.ts +0 -21
  76. package/templates/social/lumenjs.config.ts +0 -3
  77. package/templates/social/package.json +0 -5
  78. package/templates/social/pages/_layout.ts +0 -239
  79. package/templates/social/pages/apps/[id].ts +0 -173
  80. package/templates/social/pages/apps/index.ts +0 -116
  81. package/templates/social/pages/auth/login.ts +0 -92
  82. package/templates/social/pages/bookmarks.ts +0 -57
  83. package/templates/social/pages/explore.ts +0 -73
  84. package/templates/social/pages/index.ts +0 -351
  85. package/templates/social/pages/messages.ts +0 -298
  86. package/templates/social/pages/new.ts +0 -77
  87. package/templates/social/pages/notifications.ts +0 -73
  88. package/templates/social/pages/post/[id].ts +0 -124
  89. package/templates/social/pages/profile/[username].ts +0 -100
  90. package/templates/social/pages/settings/accessibility.ts +0 -153
  91. package/templates/social/pages/settings/account.ts +0 -260
  92. package/templates/social/pages/settings/help.ts +0 -141
  93. package/templates/social/pages/settings/language.ts +0 -103
  94. package/templates/social/pages/settings/privacy.ts +0 -183
  95. package/templates/social/pages/settings/security.ts +0 -133
  96. package/templates/social/pages/settings.ts +0 -185
@@ -117,8 +117,29 @@ export async function createDevServer(options) {
117
117
  setProjectDir(projectDir);
118
118
  process.env.LUMENJS_PROJECT_DIR = projectDir;
119
119
  const shared = getSharedViteConfig(projectDir, { integrations });
120
+ // Load user-defined Vite plugins from lumenjs.plugins.js (if present).
121
+ // This allows apps to add custom Vite plugins (e.g. proxy middleware)
122
+ // that run at the raw Connect level, before LumenJS's own middleware.
123
+ let userPlugins = [];
124
+ const pluginsPath = path.join(projectDir, 'lumenjs.plugins.js');
125
+ if (fs.existsSync(pluginsPath)) {
126
+ try {
127
+ // Use a temporary Vite server to load the TS file via ssrLoadModule
128
+ // would be circular, so we import it directly via dynamic import
129
+ // after Vite transforms it. Instead, read and eval the JS-compatible parts.
130
+ // Simplest: the file exports an array of plugin objects.
131
+ const pluginsMod = await import(pathToFileURL(pluginsPath).href);
132
+ const exported = pluginsMod.default || pluginsMod.plugins || pluginsMod;
133
+ if (Array.isArray(exported))
134
+ userPlugins = exported;
135
+ }
136
+ catch (err) {
137
+ console.warn(`[LumenJS] Failed to load lumenjs.plugins.js:`, err?.message);
138
+ }
139
+ }
120
140
  const server = await createViteServer({
121
141
  root: projectDir,
142
+ base,
122
143
  publicDir: fs.existsSync(publicDir) ? publicDir : undefined,
123
144
  server: {
124
145
  port,
@@ -126,13 +147,22 @@ export async function createDevServer(options) {
126
147
  strictPort: false,
127
148
  allowedHosts: true,
128
149
  cors: true,
129
- hmr: process.env.HMR_CLIENT_PORT ? { clientPort: parseInt(process.env.HMR_CLIENT_PORT), protocol: process.env.HMR_PROTOCOL || 'wss', host: process.env.HMR_HOST || undefined } : true,
150
+ hmr: process.env.HMR_CLIENT_PORT ? {
151
+ clientPort: parseInt(process.env.HMR_CLIENT_PORT),
152
+ port: parseInt(process.env.HMR_CLIENT_PORT),
153
+ ...(process.env.HMR_PROTOCOL ? { protocol: process.env.HMR_PROTOCOL } : {}),
154
+ ...(process.env.HMR_HOST ? { host: process.env.HMR_HOST } : {}),
155
+ } : true,
130
156
  fs: {
131
157
  allow: [projectDir, getLumenJSNodeModules(), path.resolve(getLumenJSNodeModules(), '..')],
132
158
  },
133
159
  },
134
160
  resolve: shared.resolve,
161
+ // 'custom' prevents Vite from adding SPA fallback and indexHtml middleware,
162
+ // which would interfere with LumenJS's own HTML handler (especially when base != '/')
163
+ appType: 'custom',
135
164
  plugins: [
165
+ ...userPlugins,
136
166
  ...(integrations.includes('auth') ? [authPlugin(projectDir)] : []),
137
167
  ...shared.plugins,
138
168
  ...(integrations.includes('communication') ? [communicationPlugin(projectDir)] : []),
@@ -142,6 +172,29 @@ export async function createDevServer(options) {
142
172
  ...(i18nConfig ? [i18nPlugin(projectDir, i18nConfig)] : []),
143
173
  ...(editorMode ? [sourceAnnotatorPlugin(projectDir), editorApiPlugin(projectDir)] : []),
144
174
  lumenSocketIOPlugin(pagesDir),
175
+ ...(base !== '/' ? [{
176
+ // Fix HMR fetch URLs when Vite runs behind a base path.
177
+ // Vite's import analysis injects createHotContext() with the full filesystem
178
+ // path (e.g. /data/user-apps/.../bb2/pages/index.ts) instead of root-relative.
179
+ // When @vite/client fetches the updated module it builds the URL as
180
+ // base + filesystemPath.slice(1) → /__app_dev/{id}/data/.../pages/index.ts
181
+ // which 404s because transformMiddleware resolves relative to root, doubling
182
+ // the path. This pre-hook middleware strips the projectDir prefix so
183
+ // baseMiddleware sees the correct root-relative path.
184
+ name: 'lumenjs-hmr-path-fix',
185
+ configureServer(server) {
186
+ const projSlash = projectDir.replace(/\\/g, '/');
187
+ server.middlewares.use((req, _res, next) => {
188
+ if (req.url) {
189
+ const prefix = base + projSlash.slice(1) + '/';
190
+ if (req.url.startsWith(prefix)) {
191
+ req.url = base + req.url.slice(prefix.length);
192
+ }
193
+ }
194
+ next();
195
+ });
196
+ },
197
+ }] : []),
145
198
  {
146
199
  // Clear SSR module cache on file changes so the next SSR request uses fresh code.
147
200
  // Without this, HMR updates the client but SSR keeps serving stale modules.
@@ -183,103 +236,108 @@ export async function createDevServer(options) {
183
236
  }
184
237
  },
185
238
  configureServer(server) {
186
- server.middlewares.use(async (req, res, next) => {
187
- const pathname = (req.url || '/').split('?')[0];
188
- if (pathname.startsWith('/@') || pathname.startsWith('/node_modules') || pathname.includes('.')) {
189
- return next();
190
- }
191
- const middlewareEntries = scanMiddleware(pagesDir);
192
- if (middlewareEntries.length === 0)
193
- return next();
194
- const matchingDirs = getMiddlewareDirsForPathname(pathname, middlewareEntries);
195
- if (matchingDirs.length === 0)
196
- return next();
197
- const allMw = [];
198
- for (const entry of matchingDirs) {
199
- try {
200
- const mod = await server.ssrLoadModule(entry.filePath);
201
- allMw.push(...extractMiddleware(mod));
239
+ return () => {
240
+ server.middlewares.use(async (req, res, next) => {
241
+ const pathname = (req.url || '/').split('?')[0];
242
+ if (pathname.startsWith('/@') || pathname.startsWith('/node_modules') || pathname.includes('.')) {
243
+ return next();
202
244
  }
203
- catch (err) {
204
- console.error(`[LumenJS] Failed to load _middleware.ts (${entry.dir || 'root'}):`, err);
245
+ const middlewareEntries = scanMiddleware(pagesDir);
246
+ if (middlewareEntries.length === 0)
247
+ return next();
248
+ const matchingDirs = getMiddlewareDirsForPathname(pathname, middlewareEntries);
249
+ if (matchingDirs.length === 0)
250
+ return next();
251
+ const allMw = [];
252
+ for (const entry of matchingDirs) {
253
+ try {
254
+ const mod = await server.ssrLoadModule(entry.filePath);
255
+ allMw.push(...extractMiddleware(mod));
256
+ }
257
+ catch (err) {
258
+ console.error(`[LumenJS] Failed to load _middleware.ts (${entry.dir || 'root'}):`, err);
259
+ }
205
260
  }
206
- }
207
- if (allMw.length === 0)
208
- return next();
209
- runMiddlewareChain(allMw, req, res, next);
210
- });
261
+ if (allMw.length === 0)
262
+ return next();
263
+ runMiddlewareChain(allMw, req, res, next);
264
+ });
265
+ };
211
266
  }
212
267
  },
213
268
  {
214
269
  name: 'lumenjs-index-html',
215
270
  configureServer(server) {
216
- server.middlewares.use((req, res, next) => {
217
- // Guard against malformed percent-encoded URLs that crash Vite's transformIndexHtml
218
- if (req.url) {
219
- try {
220
- decodeURIComponent(req.url);
221
- }
222
- catch {
223
- res.statusCode = 400;
224
- res.end('Bad Request');
225
- return;
226
- }
227
- }
228
- if (req.url && !req.url.startsWith('/@') && !req.url.startsWith('/node_modules') &&
229
- !req.url.startsWith('/api/') && !req.url.startsWith('/__nk_loader/') &&
230
- !req.url.startsWith('/__nk_i18n/') &&
231
- !req.url.includes('.') && req.method === 'GET') {
232
- let pathname = req.url.split('?')[0];
233
- // Resolve locale from URL/cookie/header
234
- let locale;
235
- let translations;
236
- if (i18nConfig) {
237
- const localeResult = resolveLocale(pathname, i18nConfig, req.headers);
238
- locale = localeResult.locale;
239
- pathname = localeResult.pathname;
240
- translations = loadTranslationsFromDisk(projectDir, locale);
241
- }
242
- const SSR_PLACEHOLDER = '<!--__NK_SSR_CONTENT__-->';
243
- ssrRenderPage(server, pagesDir, pathname, req.headers, locale, req.nkAuth?.user ?? undefined).then(async (ssrResult) => {
244
- if (ssrResult?.redirect) {
245
- res.writeHead(ssrResult.redirect.status, { Location: ssrResult.redirect.location });
246
- res.end();
271
+ return () => {
272
+ server.middlewares.use((req, res, next) => {
273
+ // Guard against malformed percent-encoded URLs that crash Vite's transformIndexHtml
274
+ if (req.url) {
275
+ try {
276
+ decodeURIComponent(req.url);
277
+ }
278
+ catch {
279
+ res.statusCode = 400;
280
+ res.end('Bad Request');
247
281
  return;
248
282
  }
249
- const shellHtml = generateIndexHtml({
250
- title,
251
- editorMode,
252
- ssrContent: ssrResult ? SSR_PLACEHOLDER : undefined,
253
- loaderData: ssrResult?.loaderData,
254
- layoutsData: ssrResult?.layoutsData,
255
- integrations,
256
- locale,
257
- i18nConfig: i18nConfig || undefined,
258
- translations,
259
- prefetch: prefetchStrategy,
260
- authUser: ssrResult?.authUser ?? req.nkAuth?.user ?? undefined,
261
- headContent,
262
- });
263
- const transformed = await server.transformIndexHtml(req.url, shellHtml);
264
- const finalHtml = ssrResult
265
- ? transformed.replace(SSR_PLACEHOLDER, ssrResult.html)
266
- : transformed;
267
- res.setHeader('Content-Type', 'text/html');
268
- res.setHeader('Cache-Control', 'no-store');
269
- res.end(finalHtml);
270
- }).catch(err => {
271
- console.error('[LumenJS] SSR/HTML generation error:', err);
272
- const html = generateIndexHtml({ title, editorMode, integrations, locale, i18nConfig: i18nConfig || undefined, translations, prefetch: prefetchStrategy, headContent });
273
- server.transformIndexHtml(req.url, html).then(transformed => {
283
+ }
284
+ if (req.url && !req.url.startsWith('/@') && !req.url.startsWith('/node_modules') &&
285
+ !req.url.startsWith('/api/') && !req.url.startsWith('/__nk_loader/') &&
286
+ !req.url.startsWith('/__nk_i18n/') &&
287
+ !req.url.includes('.') && req.method === 'GET') {
288
+ let pathname = req.url.split('?')[0];
289
+ // Resolve locale from URL/cookie/header
290
+ let locale;
291
+ let translations;
292
+ if (i18nConfig) {
293
+ const localeResult = resolveLocale(pathname, i18nConfig, req.headers);
294
+ locale = localeResult.locale;
295
+ pathname = localeResult.pathname;
296
+ translations = loadTranslationsFromDisk(projectDir, locale);
297
+ }
298
+ const SSR_PLACEHOLDER = '<!--__NK_SSR_CONTENT__-->';
299
+ ssrRenderPage(server, pagesDir, pathname, req.headers, locale, req.nkAuth?.user ?? undefined).then(async (ssrResult) => {
300
+ if (ssrResult?.redirect) {
301
+ res.writeHead(ssrResult.redirect.status, { Location: ssrResult.redirect.location });
302
+ res.end();
303
+ return;
304
+ }
305
+ const shellHtml = generateIndexHtml({
306
+ title,
307
+ editorMode,
308
+ ssrContent: ssrResult ? SSR_PLACEHOLDER : undefined,
309
+ loaderData: ssrResult?.loaderData,
310
+ layoutsData: ssrResult?.layoutsData,
311
+ integrations,
312
+ locale,
313
+ i18nConfig: i18nConfig || undefined,
314
+ translations,
315
+ prefetch: prefetchStrategy,
316
+ authUser: ssrResult?.authUser ?? req.nkAuth?.user ?? undefined,
317
+ headContent,
318
+ base,
319
+ });
320
+ const transformed = await server.transformIndexHtml(req.url, shellHtml);
321
+ const finalHtml = ssrResult
322
+ ? transformed.replace(SSR_PLACEHOLDER, ssrResult.html)
323
+ : transformed;
274
324
  res.setHeader('Content-Type', 'text/html');
275
325
  res.setHeader('Cache-Control', 'no-store');
276
- res.end(transformed);
277
- }).catch(next);
278
- });
279
- return;
280
- }
281
- next();
282
- });
326
+ res.end(finalHtml);
327
+ }).catch(err => {
328
+ console.error('[LumenJS] SSR/HTML generation error:', err);
329
+ const html = generateIndexHtml({ title, editorMode, integrations, locale, i18nConfig: i18nConfig || undefined, translations, prefetch: prefetchStrategy, headContent, base });
330
+ server.transformIndexHtml(req.url, html).then(transformed => {
331
+ res.setHeader('Content-Type', 'text/html');
332
+ res.setHeader('Cache-Control', 'no-store');
333
+ res.end(transformed);
334
+ }).catch(next);
335
+ });
336
+ return;
337
+ }
338
+ next();
339
+ });
340
+ };
283
341
  }
284
342
  }
285
343
  ],
@@ -299,7 +357,7 @@ export async function createDevServer(options) {
299
357
  },
300
358
  ssr: {
301
359
  noExternal: true,
302
- external: ['node-domexception', 'socket.io-client', 'xmlhttprequest-ssl', 'engine.io-client', 'better-sqlite3', '@lumenjs/db', '@lumenjs/permissions'],
360
+ external: ['node-domexception', 'socket.io-client', 'xmlhttprequest-ssl', 'engine.io-client', 'better-sqlite3', '@lumenjs/db', '@lumenjs/permissions', 'amqplib'],
303
361
  resolve: {
304
362
  conditions: ['node', 'import'],
305
363
  },
@@ -29,12 +29,19 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale,
29
29
  const g = globalThis;
30
30
  invalidateSsrModule(server, filePath);
31
31
  clearSsrCustomElement(g);
32
+ // Use root-relative paths for ssrLoadModule so the module graph entry
33
+ // gets url='/pages/index.ts' instead of the filesystem path. Vite's
34
+ // idToModuleMap is shared between SSR and client — if SSR creates the
35
+ // entry first with a filesystem-path URL, the client inherits it and
36
+ // HMR breaks (createHotContext gets the wrong path).
37
+ const projectRoot = path.resolve(pagesDir, '..');
38
+ const pageModuleUrl = '/' + path.relative(projectRoot, filePath).replace(/\\/g, '/');
32
39
  // Load the page module via Vite (registers the custom element, applies transforms)
33
40
  // Bypass get() so auto-define re-registers fresh classes
34
41
  const registry = g.customElements;
35
42
  if (registry)
36
43
  registry.__nk_bypass_get = true;
37
- const mod = await server.ssrLoadModule(filePath);
44
+ const mod = await server.ssrLoadModule(pageModuleUrl);
38
45
  if (registry)
39
46
  registry.__nk_bypass_get = false;
40
47
  // Run loader if present
@@ -57,9 +64,10 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale,
57
64
  // Invalidate layout module cache and clear SSR element registry
58
65
  invalidateSsrModule(server, layout.filePath);
59
66
  clearSsrCustomElement(g);
67
+ const layoutModuleUrl = '/' + path.relative(projectRoot, layout.filePath).replace(/\\/g, '/');
60
68
  if (registry)
61
69
  registry.__nk_bypass_get = true;
62
- const layoutMod = await server.ssrLoadModule(layout.filePath);
70
+ const layoutMod = await server.ssrLoadModule(layoutModuleUrl);
63
71
  if (registry)
64
72
  registry.__nk_bypass_get = false;
65
73
  let layoutLoaderData = undefined;
@@ -8,12 +8,18 @@ async function detectBackend() {
8
8
  if (resolvedBackend)
9
9
  return resolvedBackend;
10
10
  const explicit = process.env.AI_BACKEND;
11
- if (explicit === 'claude-code' || explicit === 'opencode') {
11
+ if (explicit === 'claude-code' || explicit === 'opencode' || explicit === 'deepseek') {
12
12
  resolvedBackend = explicit;
13
13
  console.log(`[LumenJS] AI backend: ${explicit} (from AI_BACKEND env)`);
14
14
  return resolvedBackend;
15
15
  }
16
- // Auto-detect: try Claude Code first (subscription-based, no server needed)
16
+ // Auto-detect: try DeepSeek if API key is set
17
+ if (process.env.DEEPSEEK_API_KEY) {
18
+ resolvedBackend = 'deepseek';
19
+ console.log('[LumenJS] AI backend: deepseek (auto-detected from DEEPSEEK_API_KEY)');
20
+ return resolvedBackend;
21
+ }
22
+ // Auto-detect: try Claude Code (subscription-based, no server needed)
17
23
  try {
18
24
  const cc = await import('./claude-code-client.js');
19
25
  const status = await cc.checkAiStatus();
@@ -36,6 +42,9 @@ async function getClient() {
36
42
  if (backend === 'claude-code') {
37
43
  return import('./claude-code-client.js');
38
44
  }
45
+ if (backend === 'deepseek') {
46
+ return import('./deepseek-client.js');
47
+ }
39
48
  return import('./opencode-client.js');
40
49
  }
41
50
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * DeepSeek AI client — uses the OpenAI-compatible chat completions API with SSE streaming.
3
+ * Configure via DEEPSEEK_API_KEY env var. Optionally set DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.
4
+ */
5
+ import type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
6
+ export declare function streamAiChat(projectDir: string, options: AiChatOptions): AiChatResult;
7
+ export declare function checkAiStatus(): Promise<AiStatusResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * DeepSeek AI client — uses the OpenAI-compatible chat completions API with SSE streaming.
3
+ * Configure via DEEPSEEK_API_KEY env var. Optionally set DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.
4
+ */
5
+ import { SYSTEM_PROMPT, buildPrompt } from './types.js';
6
+ const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || '';
7
+ const DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';
8
+ const DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';
9
+ // Simple per-session message history
10
+ const sessions = new Map();
11
+ export function streamAiChat(projectDir, options) {
12
+ const tokenCallbacks = [];
13
+ const doneCallbacks = [];
14
+ const errorCallbacks = [];
15
+ const controller = new AbortController();
16
+ let sessionId = options.sessionId || `ds-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
17
+ const enrichedPrompt = buildPrompt(options);
18
+ const run = async () => {
19
+ try {
20
+ // Build message history
21
+ if (!sessions.has(sessionId)) {
22
+ sessions.set(sessionId, [{ role: 'system', content: SYSTEM_PROMPT }]);
23
+ }
24
+ const messages = sessions.get(sessionId);
25
+ messages.push({ role: 'user', content: enrichedPrompt });
26
+ const res = await fetch(`${DEEPSEEK_BASE_URL}/v1/chat/completions`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,
31
+ },
32
+ body: JSON.stringify({
33
+ model: DEEPSEEK_MODEL,
34
+ messages,
35
+ stream: true,
36
+ }),
37
+ signal: controller.signal,
38
+ });
39
+ if (!res.ok) {
40
+ const errText = await res.text().catch(() => '');
41
+ throw new Error(`DeepSeek API error: ${res.status} ${errText}`);
42
+ }
43
+ // Parse SSE stream
44
+ const reader = res.body?.getReader();
45
+ if (!reader)
46
+ throw new Error('No response body');
47
+ const decoder = new TextDecoder();
48
+ let fullText = '';
49
+ let buffer = '';
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done)
53
+ break;
54
+ buffer += decoder.decode(value, { stream: true });
55
+ const lines = buffer.split('\n');
56
+ buffer = lines.pop() || '';
57
+ for (const line of lines) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed || !trimmed.startsWith('data:'))
60
+ continue;
61
+ const data = trimmed.slice(5).trim();
62
+ if (data === '[DONE]')
63
+ continue;
64
+ try {
65
+ const parsed = JSON.parse(data);
66
+ const delta = parsed.choices?.[0]?.delta?.content;
67
+ if (delta) {
68
+ fullText += delta;
69
+ for (const cb of tokenCallbacks)
70
+ cb(delta);
71
+ }
72
+ }
73
+ catch {
74
+ // Skip malformed SSE lines
75
+ }
76
+ }
77
+ }
78
+ // Save assistant response to history
79
+ messages.push({ role: 'assistant', content: fullText });
80
+ for (const cb of doneCallbacks)
81
+ cb(fullText);
82
+ }
83
+ catch (err) {
84
+ if (err?.name === 'AbortError')
85
+ return;
86
+ for (const cb of errorCallbacks)
87
+ cb(err instanceof Error ? err : new Error(String(err)));
88
+ }
89
+ };
90
+ run();
91
+ return {
92
+ get sessionId() { return sessionId; },
93
+ onToken: (cb) => { tokenCallbacks.push(cb); },
94
+ onDone: (cb) => { doneCallbacks.push(cb); },
95
+ onError: (cb) => { errorCallbacks.push(cb); },
96
+ abort: () => controller.abort(),
97
+ };
98
+ }
99
+ export async function checkAiStatus() {
100
+ if (!DEEPSEEK_API_KEY) {
101
+ return { configured: false, backend: 'opencode' };
102
+ }
103
+ try {
104
+ const res = await fetch(`${DEEPSEEK_BASE_URL}/v1/models`, {
105
+ headers: { 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` },
106
+ signal: AbortSignal.timeout(5000),
107
+ });
108
+ return { configured: res.ok, backend: 'opencode' };
109
+ }
110
+ catch {
111
+ return { configured: false, backend: 'opencode' };
112
+ }
113
+ }
@@ -5,7 +5,7 @@
5
5
  import type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
6
6
  /**
7
7
  * Stream an AI chat message via OpenCode's REST API.
8
- * Uses the /api/session and /api/message endpoints.
8
+ * Creates a session, sends message, and parses the response.
9
9
  */
10
10
  export declare function streamAiChat(projectDir: string, options: AiChatOptions): AiChatResult;
11
11
  /**
@@ -2,8 +2,8 @@
2
2
  * OpenCode AI client — wraps the OpenCode REST API for AI coding agent integration.
3
3
  * Connects to an OpenCode server (`opencode serve`) that handles LLM calls and file editing.
4
4
  */
5
- import { SYSTEM_PROMPT, buildPrompt } from './types.js';
6
- const OPENCODE_URL = process.env.OPENCODE_URL || 'http://localhost:3500';
5
+ import { buildPrompt } from './types.js';
6
+ const OPENCODE_URL = process.env.OPENCODE_URL || 'http://localhost:4096';
7
7
  const OPENCODE_PASSWORD = process.env.OPENCODE_SERVER_PASSWORD || '';
8
8
  function buildHeaders() {
9
9
  const headers = { 'Content-Type': 'application/json' };
@@ -14,7 +14,7 @@ function buildHeaders() {
14
14
  }
15
15
  /**
16
16
  * Stream an AI chat message via OpenCode's REST API.
17
- * Uses the /api/session and /api/message endpoints.
17
+ * Creates a session, sends message, and parses the response.
18
18
  */
19
19
  export function streamAiChat(projectDir, options) {
20
20
  const tokenCallbacks = [];
@@ -27,10 +27,10 @@ export function streamAiChat(projectDir, options) {
27
27
  try {
28
28
  // Create a new session if we don't have one
29
29
  if (!sessionId) {
30
- const createRes = await fetch(`${OPENCODE_URL}/api/session`, {
30
+ const createRes = await fetch(`${OPENCODE_URL}/session`, {
31
31
  method: 'POST',
32
32
  headers: buildHeaders(),
33
- body: JSON.stringify({ path: projectDir, system: SYSTEM_PROMPT }),
33
+ body: JSON.stringify({ path: projectDir }),
34
34
  signal: controller.signal,
35
35
  });
36
36
  if (!createRes.ok) {
@@ -39,53 +39,27 @@ export function streamAiChat(projectDir, options) {
39
39
  const sessionData = await createRes.json();
40
40
  sessionId = sessionData.id || sessionData.sessionId || '';
41
41
  }
42
- // Send message and stream response
43
- const msgRes = await fetch(`${OPENCODE_URL}/api/session/${sessionId}/message`, {
42
+ // Send message OpenCode returns JSON with parts array
43
+ const msgRes = await fetch(`${OPENCODE_URL}/session/${sessionId}/message`, {
44
44
  method: 'POST',
45
- headers: { ...buildHeaders(), 'Accept': 'text/event-stream' },
46
- body: JSON.stringify({ content: enrichedPrompt }),
45
+ headers: buildHeaders(),
46
+ body: JSON.stringify({ parts: [{ type: 'text', text: enrichedPrompt }] }),
47
47
  signal: controller.signal,
48
48
  });
49
49
  if (!msgRes.ok) {
50
- throw new Error(`OpenCode message failed: ${msgRes.status}`);
50
+ const errText = await msgRes.text().catch(() => '');
51
+ throw new Error(`OpenCode message failed: ${msgRes.status} ${errText}`);
51
52
  }
52
- const reader = msgRes.body?.getReader();
53
- if (!reader)
54
- throw new Error('No response body from OpenCode');
55
- const decoder = new TextDecoder();
53
+ // OpenCode returns JSON response with parts array
54
+ const data = await msgRes.json();
56
55
  let fullText = '';
57
- let buffer = '';
58
- while (true) {
59
- const { done, value } = await reader.read();
60
- if (done)
61
- break;
62
- buffer += decoder.decode(value, { stream: true });
63
- const lines = buffer.split('\n');
64
- buffer = lines.pop() || '';
65
- for (const line of lines) {
66
- if (line.startsWith('data: ')) {
67
- const data = line.slice(6).trim();
68
- if (data === '[DONE]')
69
- continue;
70
- try {
71
- const parsed = JSON.parse(data);
72
- // Handle different event formats from OpenCode
73
- const text = parsed.content || parsed.text || parsed.delta || '';
74
- if (text) {
75
- fullText += text;
76
- for (const cb of tokenCallbacks)
77
- cb(text);
78
- }
79
- }
80
- catch {
81
- // Non-JSON data line, treat as raw text
82
- if (data) {
83
- fullText += data;
84
- for (const cb of tokenCallbacks)
85
- cb(data);
86
- }
87
- }
88
- }
56
+ // Extract text from response parts
57
+ const parts = data.parts || [];
58
+ for (const part of parts) {
59
+ if (part.type === 'text' && part.text) {
60
+ fullText += part.text;
61
+ for (const cb of tokenCallbacks)
62
+ cb(part.text);
89
63
  }
90
64
  }
91
65
  for (const cb of doneCallbacks)
@@ -113,7 +87,7 @@ export function streamAiChat(projectDir, options) {
113
87
  */
114
88
  export async function checkAiStatus() {
115
89
  try {
116
- const res = await fetch(`${OPENCODE_URL}/api/status`, {
90
+ const res = await fetch(`${OPENCODE_URL}/global/health`, {
117
91
  headers: buildHeaders(),
118
92
  signal: AbortSignal.timeout(3000),
119
93
  });
@@ -26,5 +26,5 @@ export interface AiStatusResult {
26
26
  configured: boolean;
27
27
  backend: 'claude-code' | 'opencode';
28
28
  }
29
- export declare const SYSTEM_PROMPT = "You are an AI coding assistant working inside a LumenJS project.\n\nLumenJS is a full-stack Lit web component framework with file-based routing, server loaders, SSR, and API routes.\n\nKey conventions:\n- Pages live in `pages/` directory \u2014 file path maps to URL route\n- Components are Lit web components (LitElement) auto-registered by file path\n- Layouts: `_layout.ts` in any directory for nested layouts (use <slot>)\n- API routes: `api/` directory with named exports (GET, POST, PUT, DELETE)\n- Server loaders: `export async function loader()` for server-side data fetching\n- Styles: use Tailwind CSS classes or Lit's `static styles` with css template tag\n- Config: `lumenjs.config.ts` at project root\n\nAuto-registration:\n- Pages and layouts are auto-registered by file path \u2014 do NOT add @customElement decorators.\n `pages/about.ts` \u2192 `<page-about>`, `pages/blog/_layout.ts` \u2192 `<layout-blog>`\n\nServer loaders:\n- `export async function loader({ params, query, url, headers, locale })` at file top level.\n- Return a data object \u2192 available as `this.loaderData` on the page element.\n\nSubscribe (SSE):\n- `export async function subscribe({ params, headers, locale, push })` for real-time data.\n- Call `push(data)` to send events \u2192 available as `this.liveData` on the page element.\n\nMiddleware:\n- `_middleware.ts` in any directory applies to that route subtree.\n\nDynamic routes:\n- `[slug]` for dynamic params, `[...rest]` for catch-all.\n\nProperties:\n- Use `@property()` for public reactive props, `@state()` for internal state (from `lit/decorators.js`).\n\nExample \u2014 adding a new page (`pages/contact.ts`):\n```\nimport { LitElement, html, css } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nexport default class extends LitElement {\n static styles = css\\`/* styles here */\\`;\n render() { return html\\`<h1>Contact</h1>\\`; }\n}\n```\n\nIMPORTANT \u2014 Styling rules:\n- When asked to change a style (color, font, spacing, etc.), find and UPDATE the EXISTING CSS rule in `static styles = css\\`...\\``. Do NOT add a new class or duplicate rule.\n- Never add inline `style=\"...\"` attributes on HTML template elements. Always modify the CSS rule in `static styles`.\n- Example: to change the h1 color, find the `h1 { ... }` rule in `static styles` and update its `color` property. Do not create a new class.\n- If no CSS rule exists for the element, add one to the existing `static styles` block \u2014 do not add a separate `<style>` tag.\n\nIMPORTANT \u2014 i18n / translation rules (when the project uses i18n):\n- Text content in templates uses `t('key')` from `@lumenjs/i18n` \u2014 NEVER replace a `t()` call with hardcoded text.\n- To change displayed text, edit the translation value in `locales/<locale>.json` \u2014 do NOT modify the template.\n- Example: to change the subtitle, update `\"home.subtitle\"` in `locales/en.json` (and other locale files like `locales/fr.json`).\n- To add new text, add a key to ALL locale JSON files and use `t('new.key')` in the template.\n- The dev server watches locale files and updates the page automatically via HMR.\n\nYou have full access to the filesystem and can run shell commands.\nWhen a task requires a new npm package, install it with `npm install <package>`.\nAfter npm install, the dev server will automatically restart to load the new dependency.\nVite's HMR will pick up file changes automatically \u2014 no manual restart needed.\n\nIMPORTANT \u2014 Be fast and direct:\n- Make changes immediately \u2014 do not explain what you will do before doing it.\n- Read the file, make the edit, done. Minimize tool calls.\n- For simple CSS/text changes, edit directly without reading first if you have the source context.\n- Keep responses under 2 sentences. The user sees the diff, not your explanation.\n";
29
+ export declare const SYSTEM_PROMPT = "You are an AI coding assistant working inside a LumenJS project.\n\nLumenJS is a full-stack Lit web component framework with file-based routing, server loaders, SSR, and API routes.\n\nKey conventions:\n- Pages live in `pages/` directory \u2014 file path maps to URL route\n- Components are Lit web components (LitElement) auto-registered by file path\n- Layouts: `_layout.ts` in any directory for nested layouts (use <slot>)\n- API routes: `api/` directory with named exports (GET, POST, PUT, DELETE)\n- Server loaders: `export async function loader()` for server-side data fetching\n- Styles: use Tailwind CSS classes or Lit's `static styles` with css template tag\n- Config: `lumenjs.config.ts` at project root\n\nAuto-registration:\n- Pages and layouts are auto-registered by file path \u2014 do NOT add @customElement decorators.\n `pages/about.ts` \u2192 `<page-about>`, `pages/blog/_layout.ts` \u2192 `<layout-blog>`\n\nServer loaders:\n- `export async function loader({ params, query, url, headers, locale })` at file top level.\n- Return a data object \u2192 each key is spread as an individual property on the page element (e.g., return `{ posts }` \u2192 access as `this.posts`).\n\nSubscribe (SSE):\n- `export async function subscribe({ params, headers, locale, push })` for real-time data.\n- Call `push(data)` to send events \u2192 each key is spread as an individual property on the page element (same as loader data).\n\nMiddleware:\n- `_middleware.ts` in any directory applies to that route subtree.\n\nDynamic routes:\n- `[slug]` for dynamic params, `[...rest]` for catch-all.\n\nProperties:\n- Use `@property()` for public reactive props, `@state()` for internal state (from `lit/decorators.js`).\n\nExample \u2014 adding a new page (`pages/contact.ts`):\n```\nimport { LitElement, html, css } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nexport default class extends LitElement {\n static styles = css\\`/* styles here */\\`;\n render() { return html\\`<h1>Contact</h1>\\`; }\n}\n```\n\nIMPORTANT \u2014 Styling rules:\n- When asked to change a style (color, font, spacing, etc.), find and UPDATE the EXISTING CSS rule in `static styles = css\\`...\\``. Do NOT add a new class or duplicate rule.\n- Never add inline `style=\"...\"` attributes on HTML template elements. Always modify the CSS rule in `static styles`.\n- Example: to change the h1 color, find the `h1 { ... }` rule in `static styles` and update its `color` property. Do not create a new class.\n- If no CSS rule exists for the element, add one to the existing `static styles` block \u2014 do not add a separate `<style>` tag.\n\nIMPORTANT \u2014 i18n / translation rules (when the project uses i18n):\n- Text content in templates uses `t('key')` from `@lumenjs/i18n` \u2014 NEVER replace a `t()` call with hardcoded text.\n- To change displayed text, edit the translation value in `locales/<locale>.json` \u2014 do NOT modify the template.\n- Example: to change the subtitle, update `\"home.subtitle\"` in `locales/en.json` (and other locale files like `locales/fr.json`).\n- To add new text, add a key to ALL locale JSON files and use `t('new.key')` in the template.\n- The dev server watches locale files and updates the page automatically via HMR.\n\nYou have full access to the filesystem and can run shell commands.\nWhen a task requires a new npm package, install it with `npm install <package>`.\nAfter npm install, the dev server will automatically restart to load the new dependency.\nVite's HMR will pick up file changes automatically \u2014 no manual restart needed.\n\nIMPORTANT \u2014 Be fast and direct:\n- Make changes immediately \u2014 do not explain what you will do before doing it.\n- Read the file, make the edit, done. Minimize tool calls.\n- For simple CSS/text changes, edit directly without reading first if you have the source context.\n- Keep responses under 2 sentences. The user sees the diff, not your explanation.\n";
30
30
  export declare function buildPrompt(options: AiChatOptions): string;
@@ -20,11 +20,11 @@ Auto-registration:
20
20
 
21
21
  Server loaders:
22
22
  - \`export async function loader({ params, query, url, headers, locale })\` at file top level.
23
- - Return a data object → available as \`this.loaderData\` on the page element.
23
+ - Return a data object → each key is spread as an individual property on the page element (e.g., return \`{ posts }\` → access as \`this.posts\`).
24
24
 
25
25
  Subscribe (SSE):
26
26
  - \`export async function subscribe({ params, headers, locale, push })\` for real-time data.
27
- - Call \`push(data)\` to send events → available as \`this.liveData\` on the page element.
27
+ - Call \`push(data)\` to send events → each key is spread as an individual property on the page element (same as loader data).
28
28
 
29
29
  Middleware:
30
30
  - \`_middleware.ts\` in any directory applies to that route subtree.