@rubytech/taskmaster 1.0.105 → 1.0.107
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/agents/skills-status.js +23 -3
- package/dist/agents/skills.js +1 -0
- package/dist/agents/system-prompt.js +1 -0
- package/dist/agents/tools/memory-tool.js +2 -1
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.js +12 -1
- package/dist/control-ui/assets/index-2XyxmiR6.css +1 -0
- package/dist/control-ui/assets/{index-DtuDNTAC.js → index-B_zHmTQU.js} +823 -645
- package/dist/control-ui/assets/index-B_zHmTQU.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/control-ui/maxy-icon.png +0 -0
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/control-ui.js +111 -5
- package/dist/gateway/protocol/index.js +6 -1
- package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
- package/dist/gateway/server-close.js +2 -0
- package/dist/gateway/server-http.js +6 -1
- package/dist/gateway/server-methods/brand.js +160 -0
- package/dist/gateway/server-methods/skills.js +159 -3
- package/dist/gateway/server-methods/wifi.js +0 -10
- package/dist/gateway/server-methods-list.js +5 -0
- package/dist/gateway/server-methods.js +2 -0
- package/dist/gateway/server-wifi-watchdog.js +105 -0
- package/dist/gateway/server.impl.js +3 -0
- package/dist/memory/manager.js +12 -3
- package/package.json +1 -1
- package/skills/skill-builder/SKILL.md +97 -0
- package/skills/skill-builder/references/lean-pattern.md +118 -0
- package/skills/zero-to-prototype/SKILL.md +35 -0
- package/skills/zero-to-prototype/references/discovery.md +64 -0
- package/skills/zero-to-prototype/references/prd.md +83 -0
- package/skills/zero-to-prototype/references/validation.md +67 -0
- package/taskmaster-docs/USER-GUIDE.md +58 -2
- package/templates/customer/agents/public/AGENTS.md +3 -10
- package/templates/taskmaster/agents/public/SOUL.md +0 -4
- package/templates/tradesupport/agents/public/AGENTS.md +3 -10
- package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
- package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
- package/skills/taskmaster/SKILL.md +0 -164
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
|
2
4
|
import { installSkill } from "../../agents/skills-install.js";
|
|
3
5
|
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
|
4
|
-
import { loadWorkspaceSkillEntries } from "../../agents/skills.js";
|
|
6
|
+
import { loadWorkspaceSkillEntries, resolveBundledSkillsDir } from "../../agents/skills.js";
|
|
7
|
+
import { bumpSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|
5
8
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
|
6
9
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
|
7
|
-
import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsInstallParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
|
|
10
|
+
import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsCreateParams, validateSkillsDeleteDraftParams, validateSkillsDeleteParams, validateSkillsDraftsParams, validateSkillsInstallParams, validateSkillsReadParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
|
|
8
11
|
function listWorkspaceDirs(cfg) {
|
|
9
12
|
const dirs = new Set();
|
|
10
13
|
const list = cfg.agents?.list;
|
|
@@ -45,6 +48,21 @@ function collectSkillBins(entries) {
|
|
|
45
48
|
}
|
|
46
49
|
return [...bins].sort();
|
|
47
50
|
}
|
|
51
|
+
function resolveWorkspaceRoot(cfg) {
|
|
52
|
+
return resolveAgentWorkspaceRoot(cfg, resolveDefaultAgentId(cfg));
|
|
53
|
+
}
|
|
54
|
+
function isPreloadedSkill(skillKey) {
|
|
55
|
+
const dir = resolveBundledSkillsDir();
|
|
56
|
+
if (!dir)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
return fs.existsSync(`${dir}/${skillKey}`) &&
|
|
60
|
+
fs.statSync(`${dir}/${skillKey}`).isDirectory();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
48
66
|
export const skillsHandlers = {
|
|
49
67
|
"skills.status": ({ params, respond }) => {
|
|
50
68
|
if (!validateSkillsStatusParams(params)) {
|
|
@@ -97,6 +115,10 @@ export const skillsHandlers = {
|
|
|
97
115
|
return;
|
|
98
116
|
}
|
|
99
117
|
const p = params;
|
|
118
|
+
if (p.enabled === false && isPreloadedSkill(p.skillKey)) {
|
|
119
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be disabled"));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
100
122
|
const cfg = loadConfig();
|
|
101
123
|
const skills = cfg.skills ? { ...cfg.skills } : {};
|
|
102
124
|
const entries = skills.entries ? { ...skills.entries } : {};
|
|
@@ -134,4 +156,138 @@ export const skillsHandlers = {
|
|
|
134
156
|
await writeConfigFile(nextConfig);
|
|
135
157
|
respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined);
|
|
136
158
|
},
|
|
159
|
+
"skills.read": ({ params, respond }) => {
|
|
160
|
+
if (!validateSkillsReadParams(params)) {
|
|
161
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.read params: ${formatValidationErrors(validateSkillsReadParams.errors)}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const p = params;
|
|
165
|
+
const cfg = loadConfig();
|
|
166
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
167
|
+
const skillDir = path.join(root, "skills", p.name);
|
|
168
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
169
|
+
if (!fs.existsSync(skillFile)) {
|
|
170
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const content = fs.readFileSync(skillFile, "utf-8");
|
|
174
|
+
const references = [];
|
|
175
|
+
const refsDir = path.join(skillDir, "references");
|
|
176
|
+
if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
|
|
177
|
+
for (const entry of fs.readdirSync(refsDir, { withFileTypes: true })) {
|
|
178
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
179
|
+
references.push({
|
|
180
|
+
name: entry.name,
|
|
181
|
+
content: fs.readFileSync(path.join(refsDir, entry.name), "utf-8"),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
references.sort((a, b) => a.name.localeCompare(b.name));
|
|
186
|
+
}
|
|
187
|
+
respond(true, { name: p.name, content, references }, undefined);
|
|
188
|
+
},
|
|
189
|
+
"skills.create": async ({ params, respond }) => {
|
|
190
|
+
if (!validateSkillsCreateParams(params)) {
|
|
191
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.create params: ${formatValidationErrors(validateSkillsCreateParams.errors)}`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const p = params;
|
|
195
|
+
if (isPreloadedSkill(p.name)) {
|
|
196
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "cannot overwrite a preloaded skill"));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const cfg = loadConfig();
|
|
200
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
201
|
+
const skillDir = path.join(root, "skills", p.name);
|
|
202
|
+
// If the skill already exists (user skill), remove it first so we get a clean replace
|
|
203
|
+
if (fs.existsSync(skillDir)) {
|
|
204
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
207
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), p.skillContent, "utf-8");
|
|
208
|
+
if (p.references && p.references.length > 0) {
|
|
209
|
+
const refsDir = path.join(skillDir, "references");
|
|
210
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
211
|
+
for (const ref of p.references) {
|
|
212
|
+
const safeName = ref.name.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
213
|
+
fs.writeFileSync(path.join(refsDir, safeName), ref.content, "utf-8");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
bumpSkillsSnapshotVersion();
|
|
217
|
+
respond(true, { ok: true, name: p.name }, undefined);
|
|
218
|
+
},
|
|
219
|
+
"skills.delete": async ({ params, respond }) => {
|
|
220
|
+
if (!validateSkillsDeleteParams(params)) {
|
|
221
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.delete params: ${formatValidationErrors(validateSkillsDeleteParams.errors)}`));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const p = params;
|
|
225
|
+
if (isPreloadedSkill(p.name)) {
|
|
226
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be deleted"));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const cfg = loadConfig();
|
|
230
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
231
|
+
const skillDir = path.join(root, "skills", p.name);
|
|
232
|
+
if (!fs.existsSync(skillDir)) {
|
|
233
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
237
|
+
bumpSkillsSnapshotVersion();
|
|
238
|
+
respond(true, { ok: true, name: p.name }, undefined);
|
|
239
|
+
},
|
|
240
|
+
"skills.drafts": ({ params, respond }) => {
|
|
241
|
+
if (!validateSkillsDraftsParams(params)) {
|
|
242
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.drafts params: ${formatValidationErrors(validateSkillsDraftsParams.errors)}`));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const cfg = loadConfig();
|
|
246
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
247
|
+
const draftsDir = path.join(root, ".skill-drafts");
|
|
248
|
+
if (!fs.existsSync(draftsDir)) {
|
|
249
|
+
respond(true, { drafts: [] }, undefined);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const drafts = [];
|
|
253
|
+
for (const entry of fs.readdirSync(draftsDir, { withFileTypes: true })) {
|
|
254
|
+
if (!entry.isDirectory())
|
|
255
|
+
continue;
|
|
256
|
+
const skillFile = path.join(draftsDir, entry.name, "SKILL.md");
|
|
257
|
+
if (!fs.existsSync(skillFile))
|
|
258
|
+
continue;
|
|
259
|
+
const skillContent = fs.readFileSync(skillFile, "utf-8");
|
|
260
|
+
const references = [];
|
|
261
|
+
const refsDir = path.join(draftsDir, entry.name, "references");
|
|
262
|
+
if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
|
|
263
|
+
for (const ref of fs.readdirSync(refsDir, { withFileTypes: true })) {
|
|
264
|
+
if (ref.isFile() && ref.name.endsWith(".md")) {
|
|
265
|
+
references.push({
|
|
266
|
+
name: ref.name,
|
|
267
|
+
content: fs.readFileSync(path.join(refsDir, ref.name), "utf-8"),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
references.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
|
+
}
|
|
273
|
+
drafts.push({ name: entry.name, skillContent, references });
|
|
274
|
+
}
|
|
275
|
+
respond(true, { drafts }, undefined);
|
|
276
|
+
},
|
|
277
|
+
"skills.deleteDraft": ({ params, respond }) => {
|
|
278
|
+
if (!validateSkillsDeleteDraftParams(params)) {
|
|
279
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.deleteDraft params: ${formatValidationErrors(validateSkillsDeleteDraftParams.errors)}`));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const p = params;
|
|
283
|
+
const cfg = loadConfig();
|
|
284
|
+
const root = resolveWorkspaceRoot(cfg);
|
|
285
|
+
const draftDir = path.join(root, ".skill-drafts", p.name);
|
|
286
|
+
if (!fs.existsSync(draftDir)) {
|
|
287
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `draft "${p.name}" not found`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
fs.rmSync(draftDir, { recursive: true, force: true });
|
|
291
|
+
respond(true, { ok: true, name: p.name }, undefined);
|
|
292
|
+
},
|
|
137
293
|
};
|
|
@@ -227,16 +227,6 @@ export const wifiHandlers = {
|
|
|
227
227
|
}
|
|
228
228
|
// Check for saved connection profile
|
|
229
229
|
const saved = await getSavedWifiConnection();
|
|
230
|
-
// Auto-reconnect: if a saved profile with autoconnect exists but device
|
|
231
|
-
// is disconnected, nudge NetworkManager to bring the connection up.
|
|
232
|
-
// Fire-and-forget — the next status poll will reflect the result.
|
|
233
|
-
if (saved && saved.autoconnect && !deviceState.connected) {
|
|
234
|
-
runExec("nmcli", ["con", "up", saved.name], { timeoutMs: 30_000 })
|
|
235
|
-
.then(() => disableWifiPowerSave(context.logGateway))
|
|
236
|
-
.catch((err) => {
|
|
237
|
-
context.logGateway.warn(`wifi auto-reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
230
|
respond(true, {
|
|
241
231
|
available: true,
|
|
242
232
|
connected: deviceState.connected,
|
|
@@ -36,6 +36,7 @@ import { publicChatHandlers } from "./server-methods/public-chat.js";
|
|
|
36
36
|
import { tailscaleHandlers } from "./server-methods/tailscale.js";
|
|
37
37
|
import { wifiHandlers } from "./server-methods/wifi.js";
|
|
38
38
|
import { workspacesHandlers } from "./server-methods/workspaces.js";
|
|
39
|
+
import { brandHandlers } from "./server-methods/brand.js";
|
|
39
40
|
const ADMIN_SCOPE = "operator.admin";
|
|
40
41
|
const READ_SCOPE = "operator.read";
|
|
41
42
|
const WRITE_SCOPE = "operator.write";
|
|
@@ -236,6 +237,7 @@ export const coreGatewayHandlers = {
|
|
|
236
237
|
...memoryHandlers,
|
|
237
238
|
...recordsHandlers,
|
|
238
239
|
...workspacesHandlers,
|
|
240
|
+
...brandHandlers,
|
|
239
241
|
...publicChatHandlers,
|
|
240
242
|
...tailscaleHandlers,
|
|
241
243
|
...wifiHandlers,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side WiFi watchdog for Raspberry Pi.
|
|
3
|
+
*
|
|
4
|
+
* Runs every 30 seconds on Linux. If a saved NetworkManager WiFi profile
|
|
5
|
+
* with autoconnect=yes exists but wlan0 is disconnected, nudges NM to
|
|
6
|
+
* reconnect and disables WiFi power save.
|
|
7
|
+
*
|
|
8
|
+
* This runs in the gateway process itself — independent of any UI polling —
|
|
9
|
+
* so WiFi recovers even when no browser is connected.
|
|
10
|
+
*/
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { runExec } from "../process/exec.js";
|
|
13
|
+
const WATCHDOG_INTERVAL_MS = 30_000;
|
|
14
|
+
let timer = null;
|
|
15
|
+
export function startWifiWatchdog(log) {
|
|
16
|
+
if (os.platform() !== "linux")
|
|
17
|
+
return;
|
|
18
|
+
if (timer)
|
|
19
|
+
return;
|
|
20
|
+
// Check immediately on startup, then every 30s
|
|
21
|
+
void runWifiCheck(log);
|
|
22
|
+
timer = setInterval(() => void runWifiCheck(log), WATCHDOG_INTERVAL_MS);
|
|
23
|
+
}
|
|
24
|
+
export function stopWifiWatchdog() {
|
|
25
|
+
if (timer) {
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function runWifiCheck(log) {
|
|
31
|
+
try {
|
|
32
|
+
// Verify nmcli is available
|
|
33
|
+
try {
|
|
34
|
+
await runExec("nmcli", ["--version"], { timeoutMs: 3_000 });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return; // nmcli not installed — nothing to do
|
|
38
|
+
}
|
|
39
|
+
// Check wlan0 device state
|
|
40
|
+
const { connected } = await getWlan0State();
|
|
41
|
+
if (connected)
|
|
42
|
+
return; // All good
|
|
43
|
+
// Check for a saved WiFi profile with autoconnect enabled
|
|
44
|
+
const saved = await getSavedAutoconnectProfile();
|
|
45
|
+
if (!saved)
|
|
46
|
+
return; // No saved profile — nothing to reconnect to
|
|
47
|
+
// Saved profile exists but WiFi is down — nudge NM to reconnect
|
|
48
|
+
log.info(`wifi watchdog: reconnecting to "${saved}"…`);
|
|
49
|
+
try {
|
|
50
|
+
await runExec("nmcli", ["con", "up", saved], { timeoutMs: 30_000 });
|
|
51
|
+
log.info(`wifi watchdog: reconnected to "${saved}"`);
|
|
52
|
+
// Disable power save to prevent future drops
|
|
53
|
+
try {
|
|
54
|
+
await runExec("iw", ["dev", "wlan0", "set", "power_save", "off"], { timeoutMs: 5_000 });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Non-critical
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log.warn(`wifi watchdog: reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
log.warn(`wifi watchdog: check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function getWlan0State() {
|
|
69
|
+
try {
|
|
70
|
+
const { stdout } = await runExec("nmcli", ["-t", "-f", "GENERAL.STATE", "dev", "show", "wlan0"], { timeoutMs: 5_000 });
|
|
71
|
+
for (const line of stdout.split("\n")) {
|
|
72
|
+
if (line.startsWith("GENERAL.STATE:")) {
|
|
73
|
+
return { connected: line.includes("100") };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// wlan0 might not exist
|
|
79
|
+
}
|
|
80
|
+
return { connected: false };
|
|
81
|
+
}
|
|
82
|
+
async function getSavedAutoconnectProfile() {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await runExec("nmcli", ["-t", "-f", "NAME,TYPE,AUTOCONNECT", "con", "show"], { timeoutMs: 5_000 });
|
|
85
|
+
for (const line of stdout.split("\n")) {
|
|
86
|
+
if (!line.trim())
|
|
87
|
+
continue;
|
|
88
|
+
const placeholder = "\x00";
|
|
89
|
+
const safe = line.replace(/\\:/g, placeholder);
|
|
90
|
+
const parts = safe.split(":");
|
|
91
|
+
if (parts.length < 3)
|
|
92
|
+
continue;
|
|
93
|
+
const name = parts[0].replace(new RegExp(placeholder, "g"), ":").trim();
|
|
94
|
+
const type = parts[1].trim();
|
|
95
|
+
const autoconnect = parts[2].trim().toLowerCase() === "yes";
|
|
96
|
+
if (type === "802-11-wireless" && autoconnect) {
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Non-critical
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
@@ -50,6 +50,7 @@ import { startGatewaySidecars } from "./server-startup.js";
|
|
|
50
50
|
import { logGatewayStartup } from "./server-startup-log.js";
|
|
51
51
|
import { ensureWatchdogUnitOnStartup, scheduleWatchdogStabilityConfirmation, } from "./server-watchdog.js";
|
|
52
52
|
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
|
|
53
|
+
import { startWifiWatchdog } from "./server-wifi-watchdog.js";
|
|
53
54
|
import { loadGatewayTlsRuntime } from "./server/tls.js";
|
|
54
55
|
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
|
|
55
56
|
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
|
|
@@ -421,6 +422,8 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
421
422
|
logChannels,
|
|
422
423
|
logBrowser,
|
|
423
424
|
}));
|
|
425
|
+
// Start WiFi watchdog on Linux — auto-reconnects saved WiFi if connection drops
|
|
426
|
+
startWifiWatchdog(log.child("wifi"));
|
|
424
427
|
// Initialize memory managers for all agents and sync indexes at gateway startup
|
|
425
428
|
const logMemory = log.child("memory");
|
|
426
429
|
const agentIds = listAgentIds(cfgAtStart);
|
package/dist/memory/manager.js
CHANGED
|
@@ -74,6 +74,15 @@ function expandPathTemplate(pattern, ctx) {
|
|
|
74
74
|
result = result.replaceAll("{agentId}", ctx.agentId);
|
|
75
75
|
return result;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Ensure phone numbers in memory/users/ paths include the canonical + prefix.
|
|
79
|
+
* AI agents sometimes omit the + when constructing paths (e.g. "memory/users/447734875155/...")
|
|
80
|
+
* but the session peer and filesystem convention always use + (e.g. "+447734875155").
|
|
81
|
+
* Without this, scope patterns like memory/users/{peer}/** won't match the path.
|
|
82
|
+
*/
|
|
83
|
+
function normalizePhoneInMemoryPath(relPath) {
|
|
84
|
+
return relPath.replace(/^(memory\/users\/)(\d)/i, "$1+$2");
|
|
85
|
+
}
|
|
77
86
|
/**
|
|
78
87
|
* Simple glob pattern matcher supporting * and **.
|
|
79
88
|
* - * matches any characters except /
|
|
@@ -517,7 +526,7 @@ export class MemoryIndexManager {
|
|
|
517
526
|
return this.syncing;
|
|
518
527
|
}
|
|
519
528
|
async readFile(params) {
|
|
520
|
-
const relPath = normalizeRelPath(params.relPath);
|
|
529
|
+
const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
|
|
521
530
|
if (!relPath || !isMemoryPath(relPath)) {
|
|
522
531
|
throw new Error("path required");
|
|
523
532
|
}
|
|
@@ -552,7 +561,7 @@ export class MemoryIndexManager {
|
|
|
552
561
|
* matching the session's scope configuration.
|
|
553
562
|
*/
|
|
554
563
|
async writeFile(params) {
|
|
555
|
-
const relPath = normalizeRelPath(params.relPath);
|
|
564
|
+
const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
|
|
556
565
|
if (!relPath || !isMemoryPath(relPath)) {
|
|
557
566
|
throw new Error("path required (must be in memory/ directory)");
|
|
558
567
|
}
|
|
@@ -598,7 +607,7 @@ export class MemoryIndexManager {
|
|
|
598
607
|
let relPath;
|
|
599
608
|
if (params.destFolder) {
|
|
600
609
|
// Explicit folder — use as-is (scope checking enforces access)
|
|
601
|
-
relPath = `${params.destFolder}/${params.destFilename}
|
|
610
|
+
relPath = normalizePhoneInMemoryPath(`${params.destFolder}/${params.destFilename}`);
|
|
602
611
|
}
|
|
603
612
|
else {
|
|
604
613
|
// Default: memory/users/{peer}/media/{filename}
|
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-builder
|
|
3
|
+
description: "Guide the user through creating a new lean skill — collect name, description, behaviour rules, and references, then save as a draft for the user to install via the Control Panel."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Skill Builder
|
|
9
|
+
|
|
10
|
+
A deterministic walkthrough for creating new lean skills. Follow each step in order. Be conversational — one question at a time.
|
|
11
|
+
|
|
12
|
+
## When to Activate
|
|
13
|
+
|
|
14
|
+
The admin requests a new skill, asks to "create a skill", "add a skill", or invokes `/skill-builder`.
|
|
15
|
+
|
|
16
|
+
## Before Starting
|
|
17
|
+
|
|
18
|
+
Load `references/lean-pattern.md` for the template and examples. Use it throughout as your reference for correct structure.
|
|
19
|
+
|
|
20
|
+
## Step 1: Understand the Purpose
|
|
21
|
+
|
|
22
|
+
Ask: **"What should this skill do?"**
|
|
23
|
+
|
|
24
|
+
Listen for:
|
|
25
|
+
- What capability it adds (scheduling, quoting, inventory, etc.)
|
|
26
|
+
- When it should activate (what triggers it)
|
|
27
|
+
- What tools or data it needs (memory, web search, file access, etc.)
|
|
28
|
+
|
|
29
|
+
Clarify until you can describe the skill in one sentence. This becomes the `description` in frontmatter.
|
|
30
|
+
|
|
31
|
+
## Step 2: Choose a Name
|
|
32
|
+
|
|
33
|
+
Propose a name based on the purpose. Names must be:
|
|
34
|
+
- Lowercase
|
|
35
|
+
- Hyphen-separated (e.g. `inventory-management`)
|
|
36
|
+
- Short and descriptive
|
|
37
|
+
|
|
38
|
+
Confirm with the user.
|
|
39
|
+
|
|
40
|
+
## Step 3: Define Behaviour
|
|
41
|
+
|
|
42
|
+
Ask what rules the agent should follow when using this skill:
|
|
43
|
+
- **Tone and style** — formal, casual, WhatsApp-short?
|
|
44
|
+
- **Activation conditions** — when exactly should the agent use this skill?
|
|
45
|
+
- **Hard boundaries** — what should the agent never do?
|
|
46
|
+
- **Data sources** — where does the skill's knowledge live? (memory, external API, files)
|
|
47
|
+
- **Escalation** — when should it hand off to admin?
|
|
48
|
+
|
|
49
|
+
Not every skill needs all of these. Only include what's relevant.
|
|
50
|
+
|
|
51
|
+
## Step 4: Decide on References
|
|
52
|
+
|
|
53
|
+
If the skill has detailed procedures, templates, or data formats, those belong in `references/` files — not inline in SKILL.md.
|
|
54
|
+
|
|
55
|
+
Ask: **"Are there any detailed procedures, templates, or formats this skill needs?"**
|
|
56
|
+
|
|
57
|
+
For each reference file:
|
|
58
|
+
- Give it a descriptive filename (e.g. `event-format.md`, `quoting-rules.md`)
|
|
59
|
+
- Collect the content from the user — they can dictate, paste, or describe it and you draft it
|
|
60
|
+
|
|
61
|
+
If the skill is simple enough to fit in SKILL.md alone (under ~30 lines of instructions), skip references.
|
|
62
|
+
|
|
63
|
+
## Step 5: Compose the Skill
|
|
64
|
+
|
|
65
|
+
Using the lean pattern from `references/lean-pattern.md`, compose:
|
|
66
|
+
|
|
67
|
+
1. **SKILL.md** — frontmatter (`name`, `description`) + activation conditions + behaviour rules + references index
|
|
68
|
+
2. **Reference files** — one per detailed topic
|
|
69
|
+
|
|
70
|
+
Show the user the complete SKILL.md content and each reference file. Ask them to review.
|
|
71
|
+
|
|
72
|
+
## Step 6: Save the Draft
|
|
73
|
+
|
|
74
|
+
Use your `write` tool to save the skill as a draft:
|
|
75
|
+
|
|
76
|
+
1. Create `../../.skill-drafts/{name}/SKILL.md` with the composed content
|
|
77
|
+
2. For each reference file: create `../../.skill-drafts/{name}/references/{filename}`
|
|
78
|
+
|
|
79
|
+
The `../../` resolves from your agent directory to the workspace root. The `.skill-drafts/` folder at the workspace root is where the Control Panel looks for drafts.
|
|
80
|
+
|
|
81
|
+
**Important:** Write to `.skill-drafts/`, NOT directly to `skills/`. The user installs the skill through the Control Panel.
|
|
82
|
+
|
|
83
|
+
## Step 7: Direct to Control Panel
|
|
84
|
+
|
|
85
|
+
Tell the user:
|
|
86
|
+
|
|
87
|
+
> "Your skill draft is ready. To install it:
|
|
88
|
+
> 1. Open the **Control Panel** → **Advanced** → **Skills**
|
|
89
|
+
> 2. Click **Add Skill**
|
|
90
|
+
> 3. Your draft will appear under 'Import from draft' — click it to load
|
|
91
|
+
> 4. Review the content and click **Save Skill**
|
|
92
|
+
>
|
|
93
|
+
> Once saved, the skill will be available to agents on the next message."
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
**Remember:** Be conversational. Don't dump all questions at once. Guide through each step one at a time.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Lean SKILL.md Pattern
|
|
2
|
+
|
|
3
|
+
Skills teach agents specialised capabilities. Each skill is a directory containing a `SKILL.md` file and optional `references/` subdirectory.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
skills/
|
|
9
|
+
my-skill/
|
|
10
|
+
SKILL.md ← Main file (lean index)
|
|
11
|
+
references/
|
|
12
|
+
detailed-guide.md ← Loaded on demand via skill_read
|
|
13
|
+
templates.md
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## SKILL.md Template
|
|
17
|
+
|
|
18
|
+
```markdown
|
|
19
|
+
---
|
|
20
|
+
name: my-skill
|
|
21
|
+
description: "One-line summary of what this skill does — used for agent routing."
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# My Skill
|
|
25
|
+
|
|
26
|
+
Applies when [activation conditions].
|
|
27
|
+
|
|
28
|
+
## When to Activate
|
|
29
|
+
|
|
30
|
+
- [Trigger condition 1]
|
|
31
|
+
- [Trigger condition 2]
|
|
32
|
+
|
|
33
|
+
## Behaviour
|
|
34
|
+
|
|
35
|
+
[Concise rules — keep this section short. Move detailed procedures to references.]
|
|
36
|
+
|
|
37
|
+
## References
|
|
38
|
+
|
|
39
|
+
Load `references/detailed-guide.md` for [what it covers].
|
|
40
|
+
Load `references/templates.md` for [what it covers].
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Principles
|
|
44
|
+
|
|
45
|
+
1. **SKILL.md is an index, not a manual.** Keep it under 30 lines of instructions. The agent reads it on every activation — bloated skills waste context.
|
|
46
|
+
|
|
47
|
+
2. **References hold the detail.** Procedures, templates, data formats, and examples go in `references/`. The agent loads them on demand with `skill_read`.
|
|
48
|
+
|
|
49
|
+
3. **Frontmatter is required.** Every SKILL.md must start with YAML frontmatter containing `name` and `description`. Missing `description` causes the skill to be silently skipped.
|
|
50
|
+
|
|
51
|
+
4. **Description drives routing.** The description is injected into the agent's prompt as part of the available skills list. Make it specific enough that the agent activates the skill for the right messages.
|
|
52
|
+
|
|
53
|
+
5. **Behaviour, not data.** Skills define how the agent should act. Business-specific data (pricing, customer info, hours) belongs in memory, not in the skill file. Skills are generic; memory is specific.
|
|
54
|
+
|
|
55
|
+
## Optional Frontmatter Fields
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
metadata: {"taskmaster":{"always":true,"emoji":"📦","primaryEnv":"MY_API_KEY"}}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Field | Purpose |
|
|
62
|
+
|-------|---------|
|
|
63
|
+
| `always` | Always include in prompt (skip eligibility checks) |
|
|
64
|
+
| `emoji` | Display emoji in the Control Panel |
|
|
65
|
+
| `primaryEnv` | Env var for API key — enables the key input field in the UI |
|
|
66
|
+
| `requires.bins` | Required binaries (e.g. `["curl"]`) |
|
|
67
|
+
| `requires.env` | Required environment variables |
|
|
68
|
+
|
|
69
|
+
Most user-created skills need only `name` and `description`.
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
### Simple skill (no references)
|
|
74
|
+
|
|
75
|
+
```markdown
|
|
76
|
+
---
|
|
77
|
+
name: weather
|
|
78
|
+
description: "Provide weather context for scheduling and outdoor work decisions."
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Weather
|
|
82
|
+
|
|
83
|
+
When scheduling or planning outdoor work, check weather conditions.
|
|
84
|
+
|
|
85
|
+
## When to Activate
|
|
86
|
+
|
|
87
|
+
- Customer asks about weather
|
|
88
|
+
- Scheduling outdoor appointments
|
|
89
|
+
- Weather might affect planned work
|
|
90
|
+
|
|
91
|
+
## Behaviour
|
|
92
|
+
|
|
93
|
+
Use the `web_search` tool to check current weather for the business location.
|
|
94
|
+
Keep weather info brief — one or two sentences unless asked for detail.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Lean skill with references
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
---
|
|
101
|
+
name: event-management
|
|
102
|
+
description: "Manage anything time-bound: appointments, meetings, reminders, follow-ups."
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
# Event Management
|
|
106
|
+
|
|
107
|
+
Applies when handling anything time-bound.
|
|
108
|
+
|
|
109
|
+
## When to Activate
|
|
110
|
+
|
|
111
|
+
- Customer requests or confirms an appointment
|
|
112
|
+
- Business owner asks to schedule, reschedule, or cancel something
|
|
113
|
+
- A reminder or follow-up needs to be recorded
|
|
114
|
+
|
|
115
|
+
## References
|
|
116
|
+
|
|
117
|
+
Load `references/events.md` for the standard event template, file naming, and calendar query instructions.
|
|
118
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zero-to-prototype
|
|
3
|
+
description: "Guide business owners from a raw idea to a validated concept — customer discovery, assumption testing, value proposition, and a simple PRD. For entrepreneurs exploring new products, services, or pivots."
|
|
4
|
+
metadata: {"taskmaster":{"emoji":"🚀"}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Zero to Prototype
|
|
8
|
+
|
|
9
|
+
Helps business owners validate ideas before investing time and money. Covers the full journey from initial concept to a clear product requirement document.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Owner says they have an idea for a new product, service, or offering
|
|
14
|
+
- Owner wants to validate whether something is worth pursuing
|
|
15
|
+
- Owner asks about customer discovery, market validation, or testing assumptions
|
|
16
|
+
- Owner wants to write a PRD or product brief
|
|
17
|
+
- Phrases like "pressure test this idea", "is this worth building", "help me think through this"
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
Walk through each phase conversationally. Don't dump all phases at once — progress naturally based on where the owner is.
|
|
22
|
+
|
|
23
|
+
**Phase 1 — Capture the idea.** Understand what they want to build and why. Load `references/discovery.md`.
|
|
24
|
+
|
|
25
|
+
**Phase 2 — Validate assumptions.** Identify and test the riskiest assumptions. Load `references/validation.md`.
|
|
26
|
+
|
|
27
|
+
**Phase 3 — Define the product.** Turn validated concepts into a clear PRD. Load `references/prd.md`.
|
|
28
|
+
|
|
29
|
+
## References
|
|
30
|
+
|
|
31
|
+
Load reference files on demand as each phase is reached — not all at once.
|
|
32
|
+
|
|
33
|
+
- `references/discovery.md` — customer discovery framework, interview questions, market signals
|
|
34
|
+
- `references/validation.md` — assumption mapping, validation techniques, go/no-go criteria
|
|
35
|
+
- `references/prd.md` — PRD template, prioritisation, scope definition
|