smart-home-engine 0.11.0 → 0.12.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.
@@ -1,4 +1,4 @@
1
- import{gU as O}from"./monaco-langs-DZ6hB11b.js";import{t as I}from"./index-DD-XScWV.js";/*!-----------------------------------------------------------------------------
1
+ import{gU as O}from"./monaco-langs-DZ6hB11b.js";import{t as I}from"./index-G6QfHETZ.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -153,10 +153,10 @@
153
153
  }
154
154
  })();
155
155
  </script>
156
- <script type="module" crossorigin src="/assets/index-DD-XScWV.js"></script>
156
+ <script type="module" crossorigin src="/assets/index-G6QfHETZ.js"></script>
157
157
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-DZ6hB11b.js">
158
158
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
159
- <link rel="stylesheet" crossorigin href="/assets/index-BbwiXmS-.css">
159
+ <link rel="stylesheet" crossorigin href="/assets/index-DZTaIKZS.css">
160
160
  </head>
161
161
  <body>
162
162
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -85,3 +85,5 @@
85
85
  },
86
86
  "homepage": "https://github.com/hobbyquaker/she"
87
87
  }
88
+
89
+
package/src/web/ai-api.js CHANGED
@@ -127,7 +127,15 @@ function buildSystemPrompt(requestCtx, currentScript, store) {
127
127
  `You are SHE Assistant, an expert AI pair programmer for she (smart-home-engine).
128
128
  she is a Node.js daemon that runs user JavaScript scripts in a sandboxed VM for home automation.
129
129
  When proposing changes to a script, always output the COMPLETE new file content in a single fenced \`\`\`javascript code block. Never output partial diffs or fragments — the user applies the full file at once.
130
- Keep any existing header comments and the 'use strict'; directive.`,
130
+ Keep any existing header comments and the 'use strict'; directive.
131
+ When the user asks you to CREATE a new script (not modify the current one), place a special hint as the very first line INSIDE the code block (right after the opening \`\`\`javascript fence line), like this:
132
+ \`\`\`javascript
133
+ // @new-file: descriptive-name.js
134
+ /* global she */
135
+ 'use strict';
136
+ // ... rest of script
137
+ \`\`\`
138
+ Use a short kebab-case filename. Do NOT put the hint outside or before the code block. The UI will detect it and offer to save the file.`,
131
139
  ];
132
140
 
133
141
  if (requestCtx.apiref) {
@@ -375,6 +383,64 @@ router.get('/config', (req, res) => {
375
383
  });
376
384
  });
377
385
 
386
+ // GET /she/ai/models — list available models for the configured provider
387
+ router.get('/models', async (req, res) => {
388
+ const ai = readAiConfig(req.app.locals.configPath);
389
+ if (!ai?.provider) return res.json({ models: [] });
390
+
391
+ const base = (ai.baseUrl || 'http://localhost:11434').replace(/\/$/, '');
392
+
393
+ try {
394
+ if (ai.provider === 'ollama') {
395
+ const r = await fetch(`${base}/api/tags`);
396
+ if (!r.ok) throw new Error(`Ollama /api/tags returned ${r.status}`);
397
+ const json = await r.json();
398
+ const models = (json.models || []).map((m) => m.name || m.model).filter(Boolean).sort();
399
+ return res.json({ models });
400
+ } else if (ai.provider === 'anthropic') {
401
+ return res.json({ models: [] }); // no public list endpoint
402
+ } else {
403
+ // OpenAI / LM Studio / etc. — try /v1/models
404
+ const h = { 'Content-Type': 'application/json' };
405
+ if (ai.apiKey) h['Authorization'] = `Bearer ${ai.apiKey}`;
406
+ const r = await fetch(`${base}/v1/models`, { headers: h });
407
+ if (!r.ok) throw new Error(`/v1/models returned ${r.status}`);
408
+ const json = await r.json();
409
+ const models = (json.data || []).map((m) => m.id).filter(Boolean).sort();
410
+ return res.json({ models });
411
+ }
412
+ } catch (e) {
413
+ res.status(500).json({ error: e.message, models: [] });
414
+ }
415
+ });
416
+
417
+ // GET /she/ai/model-info — Ollama-specific: version, model details, running models
418
+ // Query param: ?model=<name> (defaults to configured model)
419
+ router.get('/model-info', async (req, res) => {
420
+ const ai = readAiConfig(req.app.locals.configPath);
421
+ if (!ai?.provider || !ai?.model) return res.status(400).json({ error: 'Not configured' });
422
+ if (ai.provider !== 'ollama') return res.status(400).json({ error: 'Model info is only available for Ollama' });
423
+
424
+ const base = (ai.baseUrl || 'http://localhost:11434').replace(/\/$/, '');
425
+ const model = (typeof req.query.model === 'string' && req.query.model) ? req.query.model : ai.model;
426
+
427
+ const [versionRes, showRes, psRes] = await Promise.allSettled([
428
+ fetch(`${base}/api/version`).then((r) => r.json()),
429
+ fetch(`${base}/api/show`, {
430
+ method: 'POST',
431
+ headers: { 'Content-Type': 'application/json' },
432
+ body: JSON.stringify({ name: model, model }),
433
+ }).then((r) => r.json()),
434
+ fetch(`${base}/api/ps`).then((r) => r.json()),
435
+ ]);
436
+
437
+ res.json({
438
+ version: versionRes.status === 'fulfilled' ? versionRes.value.version : null,
439
+ details: showRes.status === 'fulfilled' ? showRes.value.details : null,
440
+ running: psRes.status === 'fulfilled' ? (psRes.value.models || []) : null,
441
+ });
442
+ });
443
+
378
444
  // POST /she/ai/chat — non-streaming
379
445
  router.post('/chat', async (req, res) => {
380
446
  const ai = readAiConfig(req.app.locals.configPath);
@@ -382,18 +448,19 @@ router.post('/chat', async (req, res) => {
382
448
  return res.status(400).json({ error: 'AI provider not configured. Set ai.provider and ai.model in Config.' });
383
449
  }
384
450
 
385
- const { messages = [], currentScript, context = {} } = req.body || {};
451
+ const { messages = [], currentScript, context = {}, modelOverride } = req.body || {};
386
452
  if (!Array.isArray(messages)) return res.status(400).json({ error: 'messages must be an array' });
387
453
 
454
+ const aiWithModel = (modelOverride && typeof modelOverride === 'string') ? { ...ai, model: modelOverride } : ai;
388
455
  const systemPrompt = buildSystemPrompt(context, currentScript ?? null, _store);
389
456
  const fullMessages = [{ role: 'system', content: systemPrompt }, ...messages];
390
457
 
391
458
  try {
392
459
  let result;
393
460
  if (ai.provider === 'anthropic') {
394
- result = await callAnthropic(ai, fullMessages);
461
+ result = await callAnthropic(aiWithModel, fullMessages);
395
462
  } else {
396
- result = await callOpenAICompat(ai, fullMessages);
463
+ result = await callOpenAICompat(aiWithModel, fullMessages);
397
464
  }
398
465
  res.json(result);
399
466
  } catch (e) {
@@ -408,9 +475,11 @@ router.post('/chat/stream', async (req, res) => {
408
475
  return res.status(400).json({ error: 'AI provider not configured. Set ai.provider and ai.model in Config.' });
409
476
  }
410
477
 
411
- const { messages = [], currentScript, context = {} } = req.body || {};
478
+ const { messages = [], currentScript, context = {}, modelOverride } = req.body || {};
412
479
  if (!Array.isArray(messages)) return res.status(400).json({ error: 'messages must be an array' });
413
480
 
481
+ const aiWithModel = (modelOverride && typeof modelOverride === 'string') ? { ...ai, model: modelOverride } : ai;
482
+
414
483
  res.set({
415
484
  'Content-Type': 'text/event-stream',
416
485
  'Cache-Control': 'no-cache',
@@ -427,9 +496,9 @@ router.post('/chat/stream', async (req, res) => {
427
496
  const onToken = (t) => send({ token: t });
428
497
 
429
498
  if (ai.provider === 'anthropic') {
430
- await streamAnthropic(ai, fullMessages, onToken);
499
+ await streamAnthropic(aiWithModel, fullMessages, onToken);
431
500
  } else {
432
- await streamOpenAICompat(ai, fullMessages, onToken);
501
+ await streamOpenAICompat(aiWithModel, fullMessages, onToken);
433
502
  }
434
503
 
435
504
  res.write('data: [DONE]\n\n');