@folotoy/folotoy-openclaw-plugin 0.6.2 → 0.7.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/README.md +13 -0
- package/dist/cli/install.js +39 -6
- package/dist/cli/install.js.map +1 -1
- package/dist/cli/package-info.d.ts +12 -0
- package/dist/cli/package-info.d.ts.map +1 -0
- package/dist/cli/package-info.js +42 -0
- package/dist/cli/package-info.js.map +1 -0
- package/dist/cli/preset.d.ts +6 -0
- package/dist/cli/preset.d.ts.map +1 -0
- package/dist/cli/preset.js +45 -0
- package/dist/cli/preset.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/package-info.test.ts +39 -0
- package/src/__tests__/preset.test.ts +85 -0
- package/src/__tests__/test-message.mjs +5 -48
- package/src/cli/install.ts +44 -6
- package/src/cli/package-info.ts +48 -0
- package/src/cli/preset.ts +54 -0
- package/src/presets/single-soothing.json +3 -0
package/README.md
CHANGED
|
@@ -16,6 +16,19 @@ Interactive install (scan QR code to pair your toy):
|
|
|
16
16
|
npx @folotoy/folotoy-openclaw-plugin install
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
To install with a built-in preset (e.g. send only a single soothing reply
|
|
20
|
+
instead of looping until the LLM responds):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @folotoy/folotoy-openclaw-plugin install --preset single-soothing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Available presets are bundled in `src/presets/*.json`. Currently:
|
|
27
|
+
|
|
28
|
+
| Preset | Effect |
|
|
29
|
+
|--------|--------|
|
|
30
|
+
| `single-soothing` | Sets `soothing_loop_enabled = false` so only the initial `order=1` soothing reply is sent; no further soothing replies are emitted while waiting for the LLM. |
|
|
31
|
+
|
|
19
32
|
Or install manually:
|
|
20
33
|
|
|
21
34
|
```bash
|
package/dist/cli/install.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import qrcode from 'qrcode-terminal';
|
|
3
3
|
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js';
|
|
4
|
+
import { getInstallSpec, getPluginName } from './package-info.js';
|
|
5
|
+
import { loadPreset, listPresets } from './preset.js';
|
|
4
6
|
const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn';
|
|
5
7
|
const POLL_INTERVAL_MS = 3000;
|
|
6
8
|
const POLL_TIMEOUT_MS = 300_000; // 5 minutes
|
|
@@ -25,8 +27,9 @@ function installPlugin() {
|
|
|
25
27
|
catch {
|
|
26
28
|
// ignore
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const spec = getInstallSpec();
|
|
31
|
+
console.log(`Installing FoloToy plugin (${spec})...`);
|
|
32
|
+
execSync(`openclaw plugins install ${spec}`, { stdio: 'inherit' });
|
|
30
33
|
}
|
|
31
34
|
async function createSession() {
|
|
32
35
|
const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' });
|
|
@@ -73,7 +76,7 @@ function restartGateway() {
|
|
|
73
76
|
console.warn('⚠ Failed to restart gateway. You can restart manually: openclaw gateway restart');
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
|
-
function writeConfig(result) {
|
|
79
|
+
function writeConfig(result, preset = {}) {
|
|
77
80
|
execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' });
|
|
78
81
|
execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' });
|
|
79
82
|
execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' });
|
|
@@ -81,15 +84,45 @@ function writeConfig(result) {
|
|
|
81
84
|
const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT;
|
|
82
85
|
execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' });
|
|
83
86
|
execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' });
|
|
87
|
+
for (const [key, value] of Object.entries(preset)) {
|
|
88
|
+
execSync(`openclaw config set channels.folotoy.${key} ${value}`, { stdio: 'pipe' });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function parsePresetArg(argv) {
|
|
92
|
+
for (let i = 0; i < argv.length; i++) {
|
|
93
|
+
const arg = argv[i];
|
|
94
|
+
if (arg === '--preset') {
|
|
95
|
+
const value = argv[i + 1];
|
|
96
|
+
if (!value || value.startsWith('--')) {
|
|
97
|
+
throw new Error('--preset requires a name (e.g. --preset single-soothing)');
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (arg && arg.startsWith('--preset=')) {
|
|
102
|
+
return arg.slice('--preset='.length);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
84
106
|
}
|
|
85
107
|
// ── Main ───────────────────────────────────────────────
|
|
86
108
|
async function main() {
|
|
87
|
-
const
|
|
109
|
+
const argv = process.argv.slice(2);
|
|
110
|
+
const command = argv[0];
|
|
88
111
|
if (command !== 'install') {
|
|
89
|
-
|
|
112
|
+
const presets = listPresets();
|
|
113
|
+
console.log(`Usage: npx ${getPluginName()} install [--preset <name>]`);
|
|
114
|
+
if (presets.length)
|
|
115
|
+
console.log(`Available presets: ${presets.join(', ')}`);
|
|
90
116
|
process.exit(command ? 1 : 0);
|
|
91
117
|
}
|
|
118
|
+
// Resolve preset before any side effects (pairing, plugin install) so a bad
|
|
119
|
+
// preset name fails fast without making the user scan a QR.
|
|
120
|
+
const presetName = parsePresetArg(argv.slice(1));
|
|
121
|
+
const preset = presetName ? loadPreset(presetName) : {};
|
|
92
122
|
console.log('🧸 FoloToy OpenClaw Plugin Installer\n');
|
|
123
|
+
if (presetName) {
|
|
124
|
+
console.log(`Using preset: ${presetName} → ${JSON.stringify(preset)}\n`);
|
|
125
|
+
}
|
|
93
126
|
// Step 1: check prerequisites
|
|
94
127
|
console.log('Checking openclaw...');
|
|
95
128
|
checkOpenClaw();
|
|
@@ -107,7 +140,7 @@ async function main() {
|
|
|
107
140
|
const result = await pollSession(session.session_id);
|
|
108
141
|
// Step 6: write config
|
|
109
142
|
console.log('\nWriting configuration...');
|
|
110
|
-
writeConfig(result);
|
|
143
|
+
writeConfig(result, preset);
|
|
111
144
|
// Step 7: restart gateway
|
|
112
145
|
console.log('\nRestarting gateway...');
|
|
113
146
|
restartGateway();
|
package/dist/cli/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,MAAM,MAAM,iBAAiB,CAAA;AACpC,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function findPackageRoot(): string;
|
|
2
|
+
export declare function getPluginName(): string;
|
|
3
|
+
export declare function getPluginVersion(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Returns the npm spec string `<name>@<version>` to pass to
|
|
6
|
+
* `openclaw plugins install`. Pinning the exact version prevents OpenClaw
|
|
7
|
+
* from rejecting the install when `latest` happens to point at a prerelease
|
|
8
|
+
* — without this, `openclaw plugins install <bare-name>` resolves through
|
|
9
|
+
* the `latest` dist-tag and refuses prereleases for safety.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getInstallSpec(): string;
|
|
12
|
+
//# sourceMappingURL=package-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-info.d.ts","sourceRoot":"","sources":["../../src/cli/package-info.ts"],"names":[],"mappings":"AAIA,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAkBD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function findPackageRoot() {
|
|
5
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
while (true) {
|
|
7
|
+
if (existsSync(join(dir, 'package.json')))
|
|
8
|
+
return dir;
|
|
9
|
+
const parent = dirname(dir);
|
|
10
|
+
if (parent === dir)
|
|
11
|
+
throw new Error('package.json not found while resolving package root');
|
|
12
|
+
dir = parent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let cached;
|
|
16
|
+
function readPackageInfo() {
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(findPackageRoot(), 'package.json'), 'utf8'));
|
|
20
|
+
if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
|
|
21
|
+
throw new Error('package.json is missing name or version');
|
|
22
|
+
}
|
|
23
|
+
cached = { name: pkg.name, version: pkg.version };
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
export function getPluginName() {
|
|
27
|
+
return readPackageInfo().name;
|
|
28
|
+
}
|
|
29
|
+
export function getPluginVersion() {
|
|
30
|
+
return readPackageInfo().version;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the npm spec string `<name>@<version>` to pass to
|
|
34
|
+
* `openclaw plugins install`. Pinning the exact version prevents OpenClaw
|
|
35
|
+
* from rejecting the install when `latest` happens to point at a prerelease
|
|
36
|
+
* — without this, `openclaw plugins install <bare-name>` resolves through
|
|
37
|
+
* the `latest` dist-tag and refuses prereleases for safety.
|
|
38
|
+
*/
|
|
39
|
+
export function getInstallSpec() {
|
|
40
|
+
return `${getPluginName()}@${getPluginVersion()}`;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=package-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-info.js","sourceRoot":"","sources":["../../src/cli/package-info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,UAAU,eAAe;IAC7B,IAAI,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACjD,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,GAAG,CAAA;QACrD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QAC3B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;QAC1F,GAAG,GAAG,MAAM,CAAA;IACd,CAAC;AACH,CAAC;AAID,IAAI,MAA+B,CAAA;AAEnC,SAAS,eAAe;IACtB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,YAAY,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CACrB,CAAA;IAC1C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;IAC5D,CAAC;IACD,MAAM,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAA;IACjD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,eAAe,EAAE,CAAC,IAAI,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,eAAe,EAAE,CAAC,OAAO,CAAA;AAClC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,GAAG,aAAa,EAAE,IAAI,gBAAgB,EAAE,EAAE,CAAA;AACnD,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const PRESET_WHITELIST: readonly ["soothing_loop_enabled"];
|
|
2
|
+
export type PresetKey = (typeof PRESET_WHITELIST)[number];
|
|
3
|
+
export type Preset = Partial<Record<PresetKey, boolean>>;
|
|
4
|
+
export declare function listPresets(dir?: string): string[];
|
|
5
|
+
export declare function loadPreset(name: string, dir?: string): Preset;
|
|
6
|
+
//# sourceMappingURL=preset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/cli/preset.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB,oCAAqC,CAAA;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AACzD,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAA;AAMxD,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAAoB,GAAG,MAAM,EAAE,CAM/D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAoB,GAAG,MAAM,CAiC1E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { findPackageRoot } from './package-info.js';
|
|
4
|
+
export const PRESET_WHITELIST = ['soothing_loop_enabled'];
|
|
5
|
+
function presetDir() {
|
|
6
|
+
return join(findPackageRoot(), 'src', 'presets');
|
|
7
|
+
}
|
|
8
|
+
export function listPresets(dir = presetDir()) {
|
|
9
|
+
if (!existsSync(dir))
|
|
10
|
+
return [];
|
|
11
|
+
return readdirSync(dir)
|
|
12
|
+
.filter((f) => f.endsWith('.json'))
|
|
13
|
+
.map((f) => f.slice(0, -'.json'.length))
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
export function loadPreset(name, dir = presetDir()) {
|
|
17
|
+
const file = join(dir, `${name}.json`);
|
|
18
|
+
if (!existsSync(file)) {
|
|
19
|
+
const available = listPresets(dir);
|
|
20
|
+
throw new Error(`Preset "${name}" not found. Available: ${available.length ? available.join(', ') : '(none)'}`);
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
throw new Error(`Preset "${name}" is not valid JSON: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(`Preset "${name}" must be a JSON object`);
|
|
31
|
+
}
|
|
32
|
+
const allowed = PRESET_WHITELIST;
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
35
|
+
if (!allowed.includes(key)) {
|
|
36
|
+
throw new Error(`Preset "${name}" has unknown key "${key}". Allowed: ${PRESET_WHITELIST.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== 'boolean') {
|
|
39
|
+
throw new Error(`Preset "${name}" key "${key}" must be a boolean (got ${typeof value})`);
|
|
40
|
+
}
|
|
41
|
+
result[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=preset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.js","sourceRoot":"","sources":["../../src/cli/preset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,uBAAuB,CAAU,CAAA;AAIlE,SAAS,SAAS;IAChB,OAAO,IAAI,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,SAAS,EAAE;IACnD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC/B,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;SACvC,IAAI,EAAE,CAAA;AACX,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAc,SAAS,EAAE;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,CAAA;IACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;QAClC,MAAM,IAAI,KAAK,CACb,WAAW,IAAI,2BAA2B,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAC/F,CAAA;IACH,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,wBAAyB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAA;IAClF,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,yBAAyB,CAAC,CAAA;IAC3D,CAAC;IAED,MAAM,OAAO,GAAG,gBAAqC,CAAA;IACrD,MAAM,MAAM,GAAW,EAAE,CAAA;IACzB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAiC,CAAC,EAAE,CAAC;QAC7E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,sBAAsB,GAAG,eAAe,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACvG,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,UAAU,GAAG,4BAA4B,OAAO,KAAK,GAAG,CAAC,CAAA;QAC1F,CAAC;QACD,MAAM,CAAC,GAAgB,CAAC,GAAG,KAAK,CAAA;IAClC,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
findPackageRoot,
|
|
6
|
+
getInstallSpec,
|
|
7
|
+
getPluginName,
|
|
8
|
+
getPluginVersion,
|
|
9
|
+
} from '../cli/package-info.js'
|
|
10
|
+
|
|
11
|
+
describe('package-info', () => {
|
|
12
|
+
it('findPackageRoot resolves to a directory containing package.json', () => {
|
|
13
|
+
const root = findPackageRoot()
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
15
|
+
expect(typeof pkg.name).toBe('string')
|
|
16
|
+
expect(typeof pkg.version).toBe('string')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('getPluginName matches package.json name', () => {
|
|
20
|
+
const root = findPackageRoot()
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
22
|
+
expect(getPluginName()).toBe(pkg.name)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('getPluginVersion matches package.json version', () => {
|
|
26
|
+
const root = findPackageRoot()
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
28
|
+
expect(getPluginVersion()).toBe(pkg.version)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('getInstallSpec is "<name>@<version>" — pins the exact version so OpenClaw will install prereleases', () => {
|
|
32
|
+
expect(getInstallSpec()).toBe(`${getPluginName()}@${getPluginVersion()}`)
|
|
33
|
+
// Sanity: it must contain an `@` separator after the scope's leading `@`.
|
|
34
|
+
const spec = getInstallSpec()
|
|
35
|
+
const lastAt = spec.lastIndexOf('@')
|
|
36
|
+
expect(lastAt).toBeGreaterThan(0)
|
|
37
|
+
expect(spec.slice(lastAt + 1)).toMatch(/^\d+\.\d+\.\d+/)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { listPresets, loadPreset, PRESET_WHITELIST } from '../cli/preset.js'
|
|
6
|
+
|
|
7
|
+
let dir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
dir = mkdtempSync(join(tmpdir(), 'preset-test-'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function writePreset(name: string, body: unknown): void {
|
|
18
|
+
writeFileSync(join(dir, `${name}.json`), JSON.stringify(body))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('loadPreset', () => {
|
|
22
|
+
it('returns parsed preset for whitelisted boolean key', () => {
|
|
23
|
+
writePreset('single-soothing', { soothing_loop_enabled: false })
|
|
24
|
+
expect(loadPreset('single-soothing', dir)).toEqual({ soothing_loop_enabled: false })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('throws when preset file does not exist, listing available presets', () => {
|
|
28
|
+
writePreset('alpha', { soothing_loop_enabled: true })
|
|
29
|
+
writePreset('beta', { soothing_loop_enabled: false })
|
|
30
|
+
expect(() => loadPreset('missing', dir)).toThrow(/Preset "missing" not found.*alpha, beta/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('throws when preset has unknown key', () => {
|
|
34
|
+
writePreset('bad', { soothing_loop_enabled: false, summary_enabled: true })
|
|
35
|
+
expect(() => loadPreset('bad', dir)).toThrow(/unknown key "summary_enabled"/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('throws when whitelisted key has wrong type', () => {
|
|
39
|
+
writePreset('bad', { soothing_loop_enabled: 'false' })
|
|
40
|
+
expect(() => loadPreset('bad', dir)).toThrow(/must be a boolean \(got string\)/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('throws when JSON is not an object', () => {
|
|
44
|
+
writeFileSync(join(dir, 'arr.json'), '[1, 2, 3]')
|
|
45
|
+
expect(() => loadPreset('arr', dir)).toThrow(/must be a JSON object/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('throws when JSON is malformed', () => {
|
|
49
|
+
writeFileSync(join(dir, 'bad.json'), '{ not json')
|
|
50
|
+
expect(() => loadPreset('bad', dir)).toThrow(/not valid JSON/)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('whitelist contains only soothing_loop_enabled (current scope)', () => {
|
|
54
|
+
expect(PRESET_WHITELIST).toEqual(['soothing_loop_enabled'])
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('shipped presets', () => {
|
|
59
|
+
it('single-soothing resolves from default package presets dir and disables the soothing loop', () => {
|
|
60
|
+
expect(loadPreset('single-soothing')).toEqual({ soothing_loop_enabled: false })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('lists single-soothing among the default presets', () => {
|
|
64
|
+
expect(listPresets()).toContain('single-soothing')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('listPresets', () => {
|
|
69
|
+
it('returns sorted preset names without .json extension', () => {
|
|
70
|
+
writePreset('zeta', {})
|
|
71
|
+
writePreset('alpha', {})
|
|
72
|
+
writePreset('beta', {})
|
|
73
|
+
expect(listPresets(dir)).toEqual(['alpha', 'beta', 'zeta'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns empty array for nonexistent directory', () => {
|
|
77
|
+
expect(listPresets(join(dir, 'does-not-exist'))).toEqual([])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('ignores non-json files', () => {
|
|
81
|
+
writePreset('valid', {})
|
|
82
|
+
writeFileSync(join(dir, 'README.md'), 'hi')
|
|
83
|
+
expect(listPresets(dir)).toEqual(['valid'])
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -55,11 +55,6 @@ let gotSoothing = false
|
|
|
55
55
|
let gotReply = false
|
|
56
56
|
let gotFinish = false
|
|
57
57
|
let gotNotification = false
|
|
58
|
-
let soothingCount = 0
|
|
59
|
-
let firstReplyAt = 0
|
|
60
|
-
const soothingTimestamps = []
|
|
61
|
-
const replyTimestamps = []
|
|
62
|
-
const startTime = Date.now()
|
|
63
58
|
|
|
64
59
|
function validateOutboundMessage(msg) {
|
|
65
60
|
if (msg.identifier !== 'chat_output') {
|
|
@@ -95,8 +90,8 @@ function printSummary() {
|
|
|
95
90
|
console.log('='.repeat(60))
|
|
96
91
|
|
|
97
92
|
const checks = [
|
|
98
|
-
['Soothing ack
|
|
99
|
-
['AI reply
|
|
93
|
+
['Soothing ack (order=1)', gotSoothing],
|
|
94
|
+
['AI reply (order=2+)', gotReply],
|
|
100
95
|
['Finish message (is_finished=true)', gotFinish],
|
|
101
96
|
]
|
|
102
97
|
|
|
@@ -104,27 +99,6 @@ function printSummary() {
|
|
|
104
99
|
console.log(` ${ok ? '✅' : '❌'} ${label}`)
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
console.log('\n Soothing stats:')
|
|
108
|
-
console.log(` Total soothing messages: ${soothingCount}`)
|
|
109
|
-
if (soothingTimestamps.length > 0) {
|
|
110
|
-
console.log(` Soothing timestamps: ${soothingTimestamps.map(t => `+${t}ms`).join(', ')}`)
|
|
111
|
-
if (soothingTimestamps.length > 1) {
|
|
112
|
-
const intervals = soothingTimestamps.slice(1).map((t, i) => t - soothingTimestamps[i])
|
|
113
|
-
console.log(` Intervals between soothing: ${intervals.map(t => `${t}ms`).join(', ')}`)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (firstReplyAt) {
|
|
117
|
-
console.log(` First LLM reply at: +${firstReplyAt}ms`)
|
|
118
|
-
const lastSoothing = soothingTimestamps[soothingTimestamps.length - 1] || 0
|
|
119
|
-
if (lastSoothing && firstReplyAt > lastSoothing) {
|
|
120
|
-
console.log(` Gap (last soothing → first reply): ${firstReplyAt - lastSoothing}ms`)
|
|
121
|
-
}
|
|
122
|
-
console.log(` No soothing after first LLM reply: ${soothingTimestamps.every(t => t < firstReplyAt) ? '✅ YES' : '❌ NO'}`)
|
|
123
|
-
}
|
|
124
|
-
if (replyTimestamps.length > 0) {
|
|
125
|
-
console.log(` Total LLM reply chunks: ${replyTimestamps.length}`)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
102
|
if (mode === 'reminder') {
|
|
129
103
|
console.log(` ${gotNotification ? '✅' : 'ℹ️ '} Notification on event/post${gotNotification ? '' : ' (not received within timeout — may arrive later)'}`)
|
|
130
104
|
}
|
|
@@ -183,29 +157,12 @@ client.on('message', (topic, payload) => {
|
|
|
183
157
|
|
|
184
158
|
if (topic === outboundTopic) {
|
|
185
159
|
const p = msg.outParams
|
|
186
|
-
const elapsed = Date.now() - startTime
|
|
187
160
|
const label = p?.is_finished ? 'FINISH' : `REPLY(order=${p?.order})`
|
|
188
|
-
console.log(`\n[OUTBOUND ${label}]
|
|
161
|
+
console.log(`\n[OUTBOUND ${label}] ${p?.content || '(empty)'}`)
|
|
189
162
|
validateOutboundMessage(msg)
|
|
190
163
|
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
if (!firstReplyAt) {
|
|
194
|
-
// All messages before the first LLM sentence are soothing
|
|
195
|
-
// We'll determine this retrospectively, for now track all
|
|
196
|
-
}
|
|
197
|
-
// Heuristic: soothing messages are short Chinese phrases ending with ... or ~
|
|
198
|
-
const isSoothing = /[.…~]$/.test(p.content) && p.content.length < 30
|
|
199
|
-
if (isSoothing) {
|
|
200
|
-
soothingCount++
|
|
201
|
-
soothingTimestamps.push(elapsed)
|
|
202
|
-
gotSoothing = true
|
|
203
|
-
} else {
|
|
204
|
-
if (!firstReplyAt) firstReplyAt = elapsed
|
|
205
|
-
replyTimestamps.push(elapsed)
|
|
206
|
-
gotReply = true
|
|
207
|
-
}
|
|
208
|
-
}
|
|
164
|
+
if (p?.order === 1 && !p?.is_finished) gotSoothing = true
|
|
165
|
+
if (p?.order >= 2 && !p?.is_finished && p?.content) gotReply = true
|
|
209
166
|
if (p?.is_finished) {
|
|
210
167
|
gotFinish = true
|
|
211
168
|
if (mode === 'chat') {
|
package/src/cli/install.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
2
|
import qrcode from 'qrcode-terminal'
|
|
3
3
|
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js'
|
|
4
|
+
import { getInstallSpec, getPluginName } from './package-info.js'
|
|
5
|
+
import { loadPreset, listPresets, type Preset } from './preset.js'
|
|
4
6
|
|
|
5
7
|
const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn'
|
|
6
8
|
const POLL_INTERVAL_MS = 3000
|
|
@@ -40,8 +42,9 @@ function installPlugin(): void {
|
|
|
40
42
|
} catch {
|
|
41
43
|
// ignore
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const spec = getInstallSpec()
|
|
46
|
+
console.log(`Installing FoloToy plugin (${spec})...`)
|
|
47
|
+
execSync(`openclaw plugins install ${spec}`, { stdio: 'inherit' })
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
async function createSession(): Promise<CreateSessionResponse> {
|
|
@@ -95,7 +98,10 @@ function restartGateway(): void {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function writeConfig(
|
|
101
|
+
function writeConfig(
|
|
102
|
+
result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number },
|
|
103
|
+
preset: Preset = {},
|
|
104
|
+
): void {
|
|
99
105
|
execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
|
|
100
106
|
execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
|
|
101
107
|
execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' })
|
|
@@ -104,19 +110,51 @@ function writeConfig(result: { toy_sn: string; toy_key: string; mqtt_host?: stri
|
|
|
104
110
|
const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT
|
|
105
111
|
execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' })
|
|
106
112
|
execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' })
|
|
113
|
+
|
|
114
|
+
for (const [key, value] of Object.entries(preset)) {
|
|
115
|
+
execSync(`openclaw config set channels.folotoy.${key} ${value}`, { stdio: 'pipe' })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parsePresetArg(argv: readonly string[]): string | undefined {
|
|
120
|
+
for (let i = 0; i < argv.length; i++) {
|
|
121
|
+
const arg = argv[i]
|
|
122
|
+
if (arg === '--preset') {
|
|
123
|
+
const value = argv[i + 1]
|
|
124
|
+
if (!value || value.startsWith('--')) {
|
|
125
|
+
throw new Error('--preset requires a name (e.g. --preset single-soothing)')
|
|
126
|
+
}
|
|
127
|
+
return value
|
|
128
|
+
}
|
|
129
|
+
if (arg && arg.startsWith('--preset=')) {
|
|
130
|
+
return arg.slice('--preset='.length)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
// ── Main ───────────────────────────────────────────────
|
|
110
137
|
|
|
111
138
|
async function main() {
|
|
112
|
-
const
|
|
139
|
+
const argv = process.argv.slice(2)
|
|
140
|
+
const command = argv[0]
|
|
113
141
|
|
|
114
142
|
if (command !== 'install') {
|
|
115
|
-
|
|
143
|
+
const presets = listPresets()
|
|
144
|
+
console.log(`Usage: npx ${getPluginName()} install [--preset <name>]`)
|
|
145
|
+
if (presets.length) console.log(`Available presets: ${presets.join(', ')}`)
|
|
116
146
|
process.exit(command ? 1 : 0)
|
|
117
147
|
}
|
|
118
148
|
|
|
149
|
+
// Resolve preset before any side effects (pairing, plugin install) so a bad
|
|
150
|
+
// preset name fails fast without making the user scan a QR.
|
|
151
|
+
const presetName = parsePresetArg(argv.slice(1))
|
|
152
|
+
const preset: Preset = presetName ? loadPreset(presetName) : {}
|
|
153
|
+
|
|
119
154
|
console.log('🧸 FoloToy OpenClaw Plugin Installer\n')
|
|
155
|
+
if (presetName) {
|
|
156
|
+
console.log(`Using preset: ${presetName} → ${JSON.stringify(preset)}\n`)
|
|
157
|
+
}
|
|
120
158
|
|
|
121
159
|
// Step 1: check prerequisites
|
|
122
160
|
console.log('Checking openclaw...')
|
|
@@ -140,7 +178,7 @@ async function main() {
|
|
|
140
178
|
|
|
141
179
|
// Step 6: write config
|
|
142
180
|
console.log('\nWriting configuration...')
|
|
143
|
-
writeConfig(result)
|
|
181
|
+
writeConfig(result, preset)
|
|
144
182
|
|
|
145
183
|
// Step 7: restart gateway
|
|
146
184
|
console.log('\nRestarting gateway...')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export function findPackageRoot(): string {
|
|
6
|
+
let dir = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
while (true) {
|
|
8
|
+
if (existsSync(join(dir, 'package.json'))) return dir
|
|
9
|
+
const parent = dirname(dir)
|
|
10
|
+
if (parent === dir) throw new Error('package.json not found while resolving package root')
|
|
11
|
+
dir = parent
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type PackageInfo = { name: string; version: string }
|
|
16
|
+
|
|
17
|
+
let cached: PackageInfo | undefined
|
|
18
|
+
|
|
19
|
+
function readPackageInfo(): PackageInfo {
|
|
20
|
+
if (cached) return cached
|
|
21
|
+
const pkg = JSON.parse(
|
|
22
|
+
readFileSync(join(findPackageRoot(), 'package.json'), 'utf8'),
|
|
23
|
+
) as { name?: unknown; version?: unknown }
|
|
24
|
+
if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
|
|
25
|
+
throw new Error('package.json is missing name or version')
|
|
26
|
+
}
|
|
27
|
+
cached = { name: pkg.name, version: pkg.version }
|
|
28
|
+
return cached
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPluginName(): string {
|
|
32
|
+
return readPackageInfo().name
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPluginVersion(): string {
|
|
36
|
+
return readPackageInfo().version
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the npm spec string `<name>@<version>` to pass to
|
|
41
|
+
* `openclaw plugins install`. Pinning the exact version prevents OpenClaw
|
|
42
|
+
* from rejecting the install when `latest` happens to point at a prerelease
|
|
43
|
+
* — without this, `openclaw plugins install <bare-name>` resolves through
|
|
44
|
+
* the `latest` dist-tag and refuses prereleases for safety.
|
|
45
|
+
*/
|
|
46
|
+
export function getInstallSpec(): string {
|
|
47
|
+
return `${getPluginName()}@${getPluginVersion()}`
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { findPackageRoot } from './package-info.js'
|
|
4
|
+
|
|
5
|
+
export const PRESET_WHITELIST = ['soothing_loop_enabled'] as const
|
|
6
|
+
export type PresetKey = (typeof PRESET_WHITELIST)[number]
|
|
7
|
+
export type Preset = Partial<Record<PresetKey, boolean>>
|
|
8
|
+
|
|
9
|
+
function presetDir(): string {
|
|
10
|
+
return join(findPackageRoot(), 'src', 'presets')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function listPresets(dir: string = presetDir()): string[] {
|
|
14
|
+
if (!existsSync(dir)) return []
|
|
15
|
+
return readdirSync(dir)
|
|
16
|
+
.filter((f) => f.endsWith('.json'))
|
|
17
|
+
.map((f) => f.slice(0, -'.json'.length))
|
|
18
|
+
.sort()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadPreset(name: string, dir: string = presetDir()): Preset {
|
|
22
|
+
const file = join(dir, `${name}.json`)
|
|
23
|
+
if (!existsSync(file)) {
|
|
24
|
+
const available = listPresets(dir)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Preset "${name}" not found. Available: ${available.length ? available.join(', ') : '(none)'}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let parsed: unknown
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new Error(`Preset "${name}" is not valid JSON: ${(err as Error).message}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
38
|
+
throw new Error(`Preset "${name}" must be a JSON object`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const allowed = PRESET_WHITELIST as readonly string[]
|
|
42
|
+
const result: Preset = {}
|
|
43
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
44
|
+
if (!allowed.includes(key)) {
|
|
45
|
+
throw new Error(`Preset "${name}" has unknown key "${key}". Allowed: ${PRESET_WHITELIST.join(', ')}`)
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== 'boolean') {
|
|
48
|
+
throw new Error(`Preset "${name}" key "${key}" must be a boolean (got ${typeof value})`)
|
|
49
|
+
}
|
|
50
|
+
result[key as PresetKey] = value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
}
|