@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 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
@@ -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
- console.log('Installing FoloToy plugin...');
29
- execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' });
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 command = process.argv[2];
109
+ const argv = process.argv.slice(2);
110
+ const command = argv[0];
88
111
  if (command !== 'install') {
89
- console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install');
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();
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/cli/install.ts"],"names":[],"mappings":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folotoy/folotoy-openclaw-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Empower your FoloToy with OpenClaw AI capabilities.",
5
5
  "keywords": [
6
6
  "folotoy",
@@ -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 received', gotSoothing],
99
- ['AI reply received', gotReply],
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}] +${elapsed}ms | ${p?.content || '(empty)'}`)
161
+ console.log(`\n[OUTBOUND ${label}] ${p?.content || '(empty)'}`)
189
162
  validateOutboundMessage(msg)
190
163
 
191
- if (!p?.is_finished && p?.content) {
192
- // Detect soothing vs LLM reply: soothing messages arrive before LLM starts
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') {
@@ -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
- console.log('Installing FoloToy plugin...')
44
- execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' })
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(result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }): void {
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 command = process.argv[2]
139
+ const argv = process.argv.slice(2)
140
+ const command = argv[0]
113
141
 
114
142
  if (command !== 'install') {
115
- console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install')
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
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "soothing_loop_enabled": false
3
+ }