smart-home-engine 0.13.0 → 0.14.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.
- package/dist/web/assets/{index-CxNH_rV4.css → index-BcOZhXqD.css} +1 -1
- package/dist/web/assets/index-oMmhHXuR.js +220 -0
- package/dist/web/assets/{tsMode-DcNPXUSe.js → tsMode-BcZhguVQ.js} +1 -1
- package/dist/web/index.html +5 -4
- package/package.json +85 -87
- package/src/config.js +4 -0
- package/src/index.js +50 -3
- package/src/matter/controller.js +161 -23
- package/src/sandbox/matter-sandbox.js +20 -15
- package/src/sandbox/stdlib.js +17 -0
- package/src/web/ai-api.js +137 -222
- package/src/web/ai-context.js +139 -0
- package/src/web/ai-tools.js +210 -0
- package/src/web/log-ws.js +16 -1
- package/src/web/matter-api.js +7 -2
- package/src/web/prompts/db-doc.md +17 -0
- package/src/web/prompts/db-view.md +30 -0
- package/src/web/prompts/scripts-base.md +18 -0
- package/src/web/prompts/she-api-ref.md +73 -0
- package/src/web/scripts-api.js +4 -8
- package/src/web/server.js +8 -1
- package/dist/web/assets/index-UyOLwDd5.js +0 -220
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI context builder — assembles the full system prompt from per-request flags,
|
|
5
|
+
* the current editor context (script or DB view/document), and live data
|
|
6
|
+
* from the running daemon (MQTT state, sheDB, Matter devices).
|
|
7
|
+
*
|
|
8
|
+
* Kept separate from ai-api.js so prompt files and context logic can evolve
|
|
9
|
+
* independently of the HTTP routing / provider adapter code.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Load prompt templates once at startup — plain Markdown files, no escaping needed
|
|
16
|
+
const P = path.join(__dirname, 'prompts');
|
|
17
|
+
const SCRIPTS_BASE_PROMPT = fs.readFileSync(path.join(P, 'scripts-base.md'), 'utf8').trim();
|
|
18
|
+
const SHE_API_REF = fs.readFileSync(path.join(P, 'she-api-ref.md'), 'utf8').trim();
|
|
19
|
+
const DB_VIEW_PROMPT = fs.readFileSync(path.join(P, 'db-view.md'), 'utf8').trim();
|
|
20
|
+
const DB_DOC_PROMPT = fs.readFileSync(path.join(P, 'db-doc.md'), 'utf8').trim();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the full system prompt, including optional context sections.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} requestCtx
|
|
26
|
+
* { apiref, mqtt, shedb, matter, sampleDocs }
|
|
27
|
+
* @param {{ path?: string, content?: string } | null} currentScript
|
|
28
|
+
* @param {{ id?: string, filter?: string, map?: string, reduce?: string } | null} currentView
|
|
29
|
+
* @param {{ id?: string, content?: string } | null} currentDoc
|
|
30
|
+
* @param {import('../lib/state-store') | null} store
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function buildSystemPrompt(requestCtx, currentScript, currentView, currentDoc, store) {
|
|
34
|
+
const isViewMode = !!(currentView?.id);
|
|
35
|
+
const isDocMode = !!(currentDoc?.id);
|
|
36
|
+
|
|
37
|
+
let basePrompt;
|
|
38
|
+
if (isViewMode) {
|
|
39
|
+
basePrompt = DB_VIEW_PROMPT;
|
|
40
|
+
} else if (isDocMode) {
|
|
41
|
+
basePrompt = DB_DOC_PROMPT;
|
|
42
|
+
} else {
|
|
43
|
+
basePrompt = SCRIPTS_BASE_PROMPT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parts = [basePrompt];
|
|
47
|
+
|
|
48
|
+
if (requestCtx.apiref && !isViewMode && !isDocMode) {
|
|
49
|
+
parts.push(SHE_API_REF);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (currentScript?.path && typeof currentScript.content === 'string') {
|
|
53
|
+
parts.push(`## Current script: ${currentScript.path}\n\`\`\`javascript\n${currentScript.content}\n\`\`\``);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (currentView?.id) {
|
|
57
|
+
const filterStr = (currentView.filter || '').trim();
|
|
58
|
+
const mapBody = (currentView.map || '').trim();
|
|
59
|
+
const reduceBody = (currentView.reduce || '').trim();
|
|
60
|
+
const viewLines = [`## Current view: ${currentView.id}`];
|
|
61
|
+
viewLines.push(`Filter: ${filterStr || '(none)'}`);
|
|
62
|
+
viewLines.push(`Map:\n\`\`\`javascript\n${mapBody || '// (empty)'}\n\`\`\``);
|
|
63
|
+
if (reduceBody) {
|
|
64
|
+
viewLines.push(`Reduce:\n\`\`\`javascript\n${reduceBody}\n\`\`\``);
|
|
65
|
+
} else {
|
|
66
|
+
viewLines.push('Reduce: (none)');
|
|
67
|
+
}
|
|
68
|
+
parts.push(viewLines.join('\n'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (currentDoc?.id) {
|
|
72
|
+
const content = typeof currentDoc.content === 'string'
|
|
73
|
+
? currentDoc.content
|
|
74
|
+
: JSON.stringify(currentDoc.content, null, 2);
|
|
75
|
+
parts.push(`## Current document: ${currentDoc.id}\n\`\`\`json\n${content}\n\`\`\``);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (requestCtx.mqtt && store) {
|
|
79
|
+
const topics = [];
|
|
80
|
+
for (const [topic, obj] of store.mqttEntries()) {
|
|
81
|
+
topics.push(`${topic}: ${JSON.stringify(obj.val)}`);
|
|
82
|
+
if (topics.length >= 100) {
|
|
83
|
+
topics.push('… (truncated)');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (topics.length > 0) {
|
|
88
|
+
parts.push(`## Current MQTT state\n${topics.join('\n')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (requestCtx.shedb) {
|
|
93
|
+
try {
|
|
94
|
+
const core = require('./shedb').getCore();
|
|
95
|
+
if (core) {
|
|
96
|
+
const ids = Object.keys(core.docs).sort();
|
|
97
|
+
if (ids.length > 0) {
|
|
98
|
+
parts.push(`## sheDB document IDs (${ids.length} total)\n${ids.slice(0, 200).join('\n')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// shedb not initialised — skip silently
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (requestCtx.sampleDocs) {
|
|
107
|
+
try {
|
|
108
|
+
const core = require('./shedb').getCore();
|
|
109
|
+
if (core) {
|
|
110
|
+
const ids = Object.keys(core.docs).sort().slice(0, 10);
|
|
111
|
+
if (ids.length > 0) {
|
|
112
|
+
const sample = ids.map((id) => `### ${id}\n${JSON.stringify(core.docs[id], null, 2)}`).join('\n\n');
|
|
113
|
+
parts.push(`## Sample sheDB documents (${ids.length} shown)\n${sample}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// shedb not initialised — skip silently
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (requestCtx.matter) {
|
|
122
|
+
try {
|
|
123
|
+
const controller = require('../matter/controller');
|
|
124
|
+
if (typeof controller.listPaired === 'function') {
|
|
125
|
+
const nodes = controller.listPaired();
|
|
126
|
+
if (nodes.length > 0) {
|
|
127
|
+
const list = nodes.map((n) => ` nodeId ${n.nodeId}: ${n.label || 'unnamed'}`).join('\n');
|
|
128
|
+
parts.push(`## Paired Matter devices\n${list}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// matter not initialised — skip silently
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return parts.join('\n\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { buildSystemPrompt };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI tool definitions and executor for the she AI assistant.
|
|
5
|
+
*
|
|
6
|
+
* Available tools:
|
|
7
|
+
* search_mqtt_topics — fuzzy-search known MQTT topics in the state store
|
|
8
|
+
* read_script — read a script file from the scripts directory
|
|
9
|
+
* get_script_logs — retrieve recent log entries, optionally filtered by script name
|
|
10
|
+
* she_fetch — fetch a URL and return its text content
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { getLogBuffer } = require('./log-ws');
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Tool schemas
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Tool definitions in OpenAI function-calling format. */
|
|
22
|
+
const TOOL_DEFINITIONS = [
|
|
23
|
+
{
|
|
24
|
+
type: 'function',
|
|
25
|
+
function: {
|
|
26
|
+
name: 'search_mqtt_topics',
|
|
27
|
+
description:
|
|
28
|
+
'Search for MQTT topics currently tracked by the she daemon. ' +
|
|
29
|
+
'Returns matching topic names and their current values. ' +
|
|
30
|
+
'Use this to discover real topic names before writing scripts.',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
query: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Case-insensitive substring to match against topic names. Pass empty string to list all topics (capped at 50).',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['query'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'function',
|
|
45
|
+
function: {
|
|
46
|
+
name: 'read_script',
|
|
47
|
+
description:
|
|
48
|
+
'Read the content of a script file from the she scripts directory. ' +
|
|
49
|
+
'Use this to review existing scripts before suggesting changes.',
|
|
50
|
+
parameters: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
path: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Script file path relative to the scripts directory, e.g. "lights.js" or "lib/utils.js".',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ['path'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'function',
|
|
64
|
+
function: {
|
|
65
|
+
name: 'get_script_logs',
|
|
66
|
+
description:
|
|
67
|
+
'Retrieve recent log messages from the she daemon. ' +
|
|
68
|
+
'Filter by script name to diagnose errors or trace what a specific script has been doing.',
|
|
69
|
+
parameters: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
script_name: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Filter log lines to those mentioning this script name (file name without extension). Pass empty string to get all recent logs.',
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: 'integer',
|
|
78
|
+
description: 'Maximum number of log lines to return (1-200, default 50).',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: [],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'function',
|
|
87
|
+
function: {
|
|
88
|
+
name: 'she_fetch',
|
|
89
|
+
description:
|
|
90
|
+
'Fetch the content of a URL and return it as plain text. ' +
|
|
91
|
+
'Use this to retrieve documentation, data sheets, or any web resource relevant to the user request. ' +
|
|
92
|
+
'HTML is stripped to plain text automatically.',
|
|
93
|
+
parameters: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
url: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'The URL to fetch (http or https).',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: ['url'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** Same definitions in Anthropic tool format. */
|
|
108
|
+
const TOOL_DEFINITIONS_ANTHROPIC = TOOL_DEFINITIONS.map((t) => ({
|
|
109
|
+
name: t.function.name,
|
|
110
|
+
description: t.function.description,
|
|
111
|
+
input_schema: t.function.parameters,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Executor
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute a named tool and return its result as a plain string.
|
|
120
|
+
* @param {string} name — tool function name
|
|
121
|
+
* @param {object} args — parsed arguments from LLM
|
|
122
|
+
* @param {{ store: import('../lib/state-store')|null, scriptDir: string|null }} ctx
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
async function executeTool(name, args, ctx) {
|
|
126
|
+
try {
|
|
127
|
+
switch (name) {
|
|
128
|
+
case 'search_mqtt_topics': return toolSearchMqttTopics(args, ctx.store);
|
|
129
|
+
case 'read_script': return toolReadScript(args, ctx.scriptDir);
|
|
130
|
+
case 'get_script_logs': return toolGetScriptLogs(args);
|
|
131
|
+
case 'she_fetch': return await toolSheFetch(args);
|
|
132
|
+
default: return `Unknown tool: ${name}`;
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return `Tool error (${name}): ${e.message}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Individual tools
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function toolSearchMqttTopics({ query = '' }, store) {
|
|
144
|
+
if (!store) return 'MQTT state store not available.';
|
|
145
|
+
const q = query.toLowerCase();
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const [topic, obj] of store.mqttEntries()) {
|
|
148
|
+
if (!q || topic.toLowerCase().includes(q)) {
|
|
149
|
+
results.push(`${topic}: ${JSON.stringify(obj.val)}`);
|
|
150
|
+
if (results.length >= 50) {
|
|
151
|
+
results.push('… (truncated to 50 results, use a more specific query)');
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (results.length === 0) {
|
|
157
|
+
return q ? `No MQTT topics found matching "${query}".` : 'No MQTT topics tracked yet.';
|
|
158
|
+
}
|
|
159
|
+
return `Found ${results.length} topic(s):\n${results.join('\n')}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toolReadScript({ path: relPath }, scriptDir) {
|
|
163
|
+
if (!scriptDir) return 'Scripts directory not configured.';
|
|
164
|
+
if (!relPath || typeof relPath !== 'string') return 'path argument is required.';
|
|
165
|
+
const abs = path.resolve(scriptDir, relPath.replace(/^\/+/, ''));
|
|
166
|
+
// Path traversal guard
|
|
167
|
+
if (!abs.startsWith(scriptDir + path.sep) && abs !== scriptDir) {
|
|
168
|
+
return 'Access denied: path escapes the scripts directory.';
|
|
169
|
+
}
|
|
170
|
+
if (!fs.existsSync(abs)) return `File not found: ${relPath}`;
|
|
171
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
172
|
+
return `## ${relPath}\n\`\`\`javascript\n${content}\n\`\`\``;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toolGetScriptLogs({ script_name = '', limit = 50 }) {
|
|
176
|
+
const cap = Math.min(Math.max(1, Number(limit) || 50), 200);
|
|
177
|
+
const buf = getLogBuffer();
|
|
178
|
+
const needle = String(script_name).toLowerCase();
|
|
179
|
+
const filtered = needle ? buf.filter((e) => e.msg.toLowerCase().includes(needle)) : buf;
|
|
180
|
+
const recent = filtered.slice(-cap);
|
|
181
|
+
if (recent.length === 0) {
|
|
182
|
+
return needle ? `No log entries found mentioning "${script_name}".` : 'No log entries in buffer yet.';
|
|
183
|
+
}
|
|
184
|
+
return recent
|
|
185
|
+
.map((e) => {
|
|
186
|
+
const t = new Date(e.ts).toISOString().slice(11, 19);
|
|
187
|
+
return `[${t}] ${e.level.toUpperCase()} ${e.msg}`;
|
|
188
|
+
})
|
|
189
|
+
.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const MAX_FETCH_CHARS = 8000;
|
|
193
|
+
|
|
194
|
+
async function toolSheFetch({ url }) {
|
|
195
|
+
if (!url || typeof url !== 'string') return 'url argument is required.';
|
|
196
|
+
if (!/^https?:\/\//i.test(url)) return 'Only http and https URLs are supported.';
|
|
197
|
+
const res = await fetch(url, {
|
|
198
|
+
headers: { 'User-Agent': 'she-ai-agent/1.0' },
|
|
199
|
+
signal: AbortSignal.timeout(15000),
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) return `HTTP error ${res.status} ${res.statusText} fetching ${url}`;
|
|
202
|
+
const ct = res.headers.get('content-type') || '';
|
|
203
|
+
const text = await res.text();
|
|
204
|
+
// Strip HTML tags for cleaner text
|
|
205
|
+
const plain = ct.includes('html') ? text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() : text;
|
|
206
|
+
const truncated = plain.length > MAX_FETCH_CHARS ? plain.slice(0, MAX_FETCH_CHARS) + `\n… (truncated, ${plain.length} chars total)` : plain;
|
|
207
|
+
return `Content of ${url}:\n\n${truncated}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { TOOL_DEFINITIONS, TOOL_DEFINITIONS_ANTHROPIC, executeTool };
|
package/src/web/log-ws.js
CHANGED
|
@@ -5,6 +5,10 @@ const { WebSocketServer } = require('ws');
|
|
|
5
5
|
let _wss = null;
|
|
6
6
|
const _clients = new Set();
|
|
7
7
|
|
|
8
|
+
// Ring buffer of recent log entries for the AI tool get_script_logs
|
|
9
|
+
const _logBuffer = [];
|
|
10
|
+
const LOG_BUFFER_MAX = 500;
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* Attach a WebSocketServer to an existing http.Server.
|
|
10
14
|
* Clients connect to /she/ws. Once connected they receive:
|
|
@@ -54,12 +58,23 @@ function broadcast(msg) {
|
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* Broadcast a structured log entry to all connected WebSocket clients.
|
|
61
|
+
* Also stores the entry in the in-memory ring buffer.
|
|
57
62
|
* @param {{ level: string, msg: string, ts: number }} entry
|
|
58
63
|
*/
|
|
59
64
|
function broadcastLog(entry) {
|
|
65
|
+
_logBuffer.push(entry);
|
|
66
|
+
if (_logBuffer.length > LOG_BUFFER_MAX) _logBuffer.shift();
|
|
60
67
|
broadcast({ type: 'log', ...entry });
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Return a snapshot of recent log entries (newest last).
|
|
72
|
+
* @returns {{ level: string, msg: string, ts: number }[]}
|
|
73
|
+
*/
|
|
74
|
+
function getLogBuffer() {
|
|
75
|
+
return _logBuffer.slice();
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
/**
|
|
64
79
|
* Close the WebSocket server.
|
|
65
80
|
* @returns {Promise<void>}
|
|
@@ -75,4 +90,4 @@ function closeWss() {
|
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
module.exports = { attachWss, broadcast, broadcastLog, closeWss };
|
|
93
|
+
module.exports = { attachWss, broadcast, broadcastLog, closeWss, getLogBuffer };
|
package/src/web/matter-api.js
CHANGED
|
@@ -63,8 +63,13 @@ router.post('/commission', async (req, res) => {
|
|
|
63
63
|
router.get('/devices/:nodeId', (req, res) => {
|
|
64
64
|
if (!isReady()) return notReady(res);
|
|
65
65
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
66
|
+
const ctrl = getController();
|
|
67
|
+
const endpoints = ctrl.getEndpoints(req.params.nodeId);
|
|
68
|
+
// Derive device-level name and subtitle from the root endpoint (0)
|
|
69
|
+
const rootEp = endpoints.find(ep => ep.endpointId === 0);
|
|
70
|
+
const name = rootEp?.name ?? endpoints.find(ep => ep.name)?.name ?? null;
|
|
71
|
+
const subtitle = ctrl.getDeviceSubtitle(req.params.nodeId);
|
|
72
|
+
res.json({ nodeId: req.params.nodeId, endpoints, name, subtitle });
|
|
68
73
|
} catch (err) {
|
|
69
74
|
if (err.message.includes('not found')) return res.status(404).json({ error: err.message });
|
|
70
75
|
res.status(500).json({ error: err.message });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
You are SHE Assistant, helping manage sheDB documents for she (smart-home-engine).
|
|
2
|
+
|
|
3
|
+
sheDB is a simple JSON document store. Each document has a string ID (structured like an MQTT topic path, e.g. `devices/lamp1`) and a JSON value (any object, array, or scalar).
|
|
4
|
+
|
|
5
|
+
When proposing a new or updated document, output the document ID and the JSON content in this format:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
// @doc-id: devices/lamp1
|
|
9
|
+
{
|
|
10
|
+
"name": "Living room lamp",
|
|
11
|
+
"type": "light"
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `// @doc-id:` comment must be the very first line inside the JSON block — the UI uses it to pre-fill the document ID field.
|
|
16
|
+
|
|
17
|
+
Document IDs follow MQTT topic conventions: use `/` as separator, lowercase, no spaces.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
You are SHE Assistant, helping write sheDB MapReduce view definitions for she (smart-home-engine).
|
|
2
|
+
|
|
3
|
+
A view has three optional parts:
|
|
4
|
+
|
|
5
|
+
1. **Filter** — an MQTT-style topic wildcard that selects which document IDs enter the view. Plain string, no code.
|
|
6
|
+
MQTT wildcards: `+` matches exactly one path segment, `#` matches any number of segments (must be last).
|
|
7
|
+
Examples: `devices/+/state` matches `devices/lamp1/state`. `sensors/#` matches all IDs starting with `sensors/`.
|
|
8
|
+
⚠️ `*` is NOT a valid MQTT wildcard — never use it. Use `#` for "match everything".
|
|
9
|
+
|
|
10
|
+
2. **Map** — a JavaScript function body. `this` is the current document. Call `emit(value)` to include a value in the result array. No `return`.
|
|
11
|
+
|
|
12
|
+
3. **Reduce** — a JavaScript function body that receives `result` (the array from map) and must `return` a transformed value.
|
|
13
|
+
|
|
14
|
+
When proposing view parts, use these exact formats (include only the parts that change):
|
|
15
|
+
|
|
16
|
+
```filter
|
|
17
|
+
devices/#
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
// @view-map
|
|
22
|
+
if (this.temperature !== undefined) emit(this.temperature);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// @view-reduce
|
|
27
|
+
return result.reduce((a, b) => a + b, 0) / result.length;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Keep the `// @view-map` / `// @view-reduce` comment as the very first line of each block — the UI uses it to detect which field to fill in.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
You are SHE Assistant, an expert AI pair programmer for she (smart-home-engine).
|
|
2
|
+
she is a Node.js daemon that runs user JavaScript scripts in a sandboxed VM for home automation.
|
|
3
|
+
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.
|
|
4
|
+
Keep any existing header comments and the 'use strict'; directive.
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// @new-file: descriptive-name.js
|
|
9
|
+
/* global she */
|
|
10
|
+
'use strict';
|
|
11
|
+
// ... rest of script
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
### MQTT publishing rules
|
|
17
|
+
When sending a command to a device (e.g. turning a light on/off, triggering an action), ALWAYS use `she.mqtt.pub()` WITHOUT retain — never set `retain: true` on command topics.
|
|
18
|
+
Only use `she.mqtt.set()` or `retain: true` for storing persistent state or configuration values that must survive restarts, not for commands.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
## she sandbox API
|
|
2
|
+
|
|
3
|
+
Scripts run in a sandboxed VM. The `she` object is injected automatically.
|
|
4
|
+
|
|
5
|
+
### Script conventions
|
|
6
|
+
- First lines: /* global she */ then 'use strict';
|
|
7
|
+
- No require() — the module system is not available
|
|
8
|
+
- All subscriptions and schedules persist across reconnects
|
|
9
|
+
|
|
10
|
+
### MQTT
|
|
11
|
+
```
|
|
12
|
+
she.mqtt.sub(topic, [opts], cb) Subscribe; wildcards: + (1 level) # (multi)
|
|
13
|
+
+//sensor → +/status/sensor shorthand
|
|
14
|
+
opts.change: true = only fire when value changes
|
|
15
|
+
she.mqtt.pub(topic, payload, [opts]) Publish; opts: { qos, retain }
|
|
16
|
+
she.mqtt.get(topic) Current retained value (sync)
|
|
17
|
+
she.mqtt.set(topic, val) Publish as retained
|
|
18
|
+
she.mqtt.link(src, target, [fn]) Forward src changes to target; optional transform
|
|
19
|
+
she.mqtt.age(topic) Seconds since topic last received a message
|
|
20
|
+
she.mqtt.on('connect'|'disconnect', cb) MQTT lifecycle events
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Scheduling
|
|
24
|
+
```
|
|
25
|
+
she.schedule(pattern, [opts], cb)
|
|
26
|
+
pattern: cron string | Date | suncalc event name
|
|
27
|
+
suncalc events: 'sunrise' 'sunset' 'dawn' 'dusk'
|
|
28
|
+
'nauticalDawn' 'nauticalDusk' 'solarNoon' 'night'
|
|
29
|
+
opts.shift: seconds offset (e.g. -1800 = 30 min before event)
|
|
30
|
+
opts.random: max random delay in seconds added to the trigger time
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Universal key-value API
|
|
34
|
+
```
|
|
35
|
+
she.on(key, cb) Subscribe. Key prefixes: mqtt:: var:: matter::
|
|
36
|
+
she.set(key, val) Set value (mqtt:: or var:: namespaces)
|
|
37
|
+
she.get(key) Current value
|
|
38
|
+
she.getObject(key) Current { val, ts, lc } state object
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Variable system (var:: namespace)
|
|
42
|
+
Topics prefixed with "var" (default) are persisted as retained MQTT messages
|
|
43
|
+
and available across scripts via she.get('var::name') / she.set('var::name', v).
|
|
44
|
+
|
|
45
|
+
### sheDB
|
|
46
|
+
```
|
|
47
|
+
she.db.get(id) Get document (undefined if not found)
|
|
48
|
+
she.db.set(id, doc) Create or overwrite document
|
|
49
|
+
she.db.extend(id, partial) Deep-merge partial into existing document
|
|
50
|
+
she.db.delete(id) Delete document
|
|
51
|
+
she.db.sub(pattern, cb) Subscribe to document changes (MQTT wildcard)
|
|
52
|
+
she.db.query(filter, mapFn, [reduceFn]) Synchronous ad-hoc query → Array
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Matter
|
|
56
|
+
```
|
|
57
|
+
she.matter.sub(nodeId, endpointId, cluster, attr, cb) Subscribe to attribute
|
|
58
|
+
she.matter.unsub(listenerId)
|
|
59
|
+
she.matter.get(nodeId, endpointId, cluster, attr) → Promise<value>
|
|
60
|
+
she.matter.send(nodeId, endpointId, cluster, cmd, [args]) → Promise<result>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Helpers
|
|
64
|
+
```
|
|
65
|
+
she.timer(src, target, ms) Pulse target=1 for ms after src goes truthy
|
|
66
|
+
she.combineBool(srcs[], target) Publish OR of source values to target
|
|
67
|
+
she.combineMax(srcs[], target) Publish maximum of source values to target
|
|
68
|
+
she.link(src, target, [fn]) Alias for she.mqtt.link
|
|
69
|
+
she.age(topic) Alias for she.mqtt.age
|
|
70
|
+
she.now() Current timestamp in ms
|
|
71
|
+
she.debug / .info / .warn / .error Structured logging (prefixed with script name)
|
|
72
|
+
she.global Shared mutable object across all scripts
|
|
73
|
+
```
|
package/src/web/scripts-api.js
CHANGED
|
@@ -28,7 +28,7 @@ function hasShelibMarker(absDir) {
|
|
|
28
28
|
return fs.existsSync(path.join(absDir, '.shelib'));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Flat list of all
|
|
31
|
+
/** Flat list of all files with metadata and lib flag. */
|
|
32
32
|
function walk(dir, base, parentIsLib) {
|
|
33
33
|
let entries;
|
|
34
34
|
try {
|
|
@@ -43,7 +43,7 @@ function walk(dir, base, parentIsLib) {
|
|
|
43
43
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
44
44
|
if (entry.isDirectory()) {
|
|
45
45
|
results.push(...walk(path.join(dir, entry.name), rel, lib));
|
|
46
|
-
} else
|
|
46
|
+
} else {
|
|
47
47
|
const stat = fs.statSync(path.join(dir, entry.name));
|
|
48
48
|
results.push({ path: rel, size: stat.size, mtime: stat.mtimeMs, lib });
|
|
49
49
|
}
|
|
@@ -52,7 +52,7 @@ function walk(dir, base, parentIsLib) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Nested tree of all
|
|
55
|
+
* Nested tree of all files and subdirectories.
|
|
56
56
|
* Each node: { type:'file'|'dir', name, path, lib, size?, mtime?, children? }
|
|
57
57
|
*/
|
|
58
58
|
function buildTree(dir, base, parentIsLib) {
|
|
@@ -73,7 +73,7 @@ function buildTree(dir, base, parentIsLib) {
|
|
|
73
73
|
const childIsLib = lib || hasShelibMarker(abs);
|
|
74
74
|
const children = buildTree(abs, rel, childIsLib);
|
|
75
75
|
result.push({ type: 'dir', name: entry.name, path: rel, lib: childIsLib, children });
|
|
76
|
-
} else
|
|
76
|
+
} else {
|
|
77
77
|
const stat = fs.statSync(abs);
|
|
78
78
|
result.push({ type: 'file', name: entry.name, path: rel, lib, size: stat.size, mtime: stat.mtimeMs });
|
|
79
79
|
}
|
|
@@ -120,10 +120,6 @@ router.use((req, res) => {
|
|
|
120
120
|
if (method === 'PUT') {
|
|
121
121
|
const abs = safePath(root, filePath);
|
|
122
122
|
if (!abs) return res.status(400).json({ error: 'Invalid path' });
|
|
123
|
-
const basename = path.basename(abs);
|
|
124
|
-
if (!basename.endsWith('.js') && basename !== '.shelib') {
|
|
125
|
-
return res.status(400).json({ error: 'Only .js and .shelib files are allowed' });
|
|
126
|
-
}
|
|
127
123
|
const content = typeof req.body?.content === 'string' ? req.body.content : null;
|
|
128
124
|
if (content === null) return res.status(400).json({ error: 'Missing body.content string' });
|
|
129
125
|
try {
|
package/src/web/server.js
CHANGED
|
@@ -58,6 +58,13 @@ app.post('/she/restart', (req, res) => {
|
|
|
58
58
|
setTimeout(() => process.exit(0), 200);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
// Runtime stats — script count + MQTT topic count
|
|
62
|
+
let _getStats = null;
|
|
63
|
+
function setStatsProvider(fn) { _getStats = fn; }
|
|
64
|
+
app.get('/she/status', (req, res) => {
|
|
65
|
+
res.json(_getStats ? _getStats() : { scripts: 0, topics: 0 });
|
|
66
|
+
});
|
|
67
|
+
|
|
61
68
|
// Serve the built Svelte SPA from dist/web/
|
|
62
69
|
// Hashed assets (JS/CSS) are immutable; index.html must never be cached so
|
|
63
70
|
// browsers always pick up a freshly deployed version.
|
|
@@ -136,4 +143,4 @@ function stopServer() {
|
|
|
136
143
|
});
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
module.exports = { app, registerRoute, startServer, stopServer };
|
|
146
|
+
module.exports = { app, registerRoute, setStatsProvider, startServer, stopServer };
|