@folotoy/folotoy-openclaw-plugin 0.7.0 → 0.8.1

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.
Files changed (40) hide show
  1. package/README.md +11 -3
  2. package/bin/folotoy.mjs +7 -1
  3. package/dist/config-schema.d.ts +33 -0
  4. package/dist/config-schema.d.ts.map +1 -0
  5. package/dist/config-schema.js +50 -0
  6. package/dist/config-schema.js.map +1 -0
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +24 -37
  12. package/dist/index.js.map +1 -1
  13. package/openclaw.plugin.json +100 -41
  14. package/package.json +10 -4
  15. package/src/config-schema.ts +71 -0
  16. package/src/config.ts +1 -1
  17. package/src/index.ts +25 -37
  18. package/dist/cli/install.d.ts +0 -2
  19. package/dist/cli/install.d.ts.map +0 -1
  20. package/dist/cli/install.js +0 -155
  21. package/dist/cli/install.js.map +0 -1
  22. package/dist/cli/package-info.d.ts +0 -12
  23. package/dist/cli/package-info.d.ts.map +0 -1
  24. package/dist/cli/package-info.js +0 -42
  25. package/dist/cli/package-info.js.map +0 -1
  26. package/dist/cli/preset.d.ts +0 -6
  27. package/dist/cli/preset.d.ts.map +0 -1
  28. package/dist/cli/preset.js +0 -45
  29. package/dist/cli/preset.js.map +0 -1
  30. package/src/__tests__/channel.test.ts +0 -126
  31. package/src/__tests__/mqtt.test.ts +0 -14
  32. package/src/__tests__/package-info.test.ts +0 -39
  33. package/src/__tests__/preset.test.ts +0 -85
  34. package/src/__tests__/soothing.test.ts +0 -47
  35. package/src/__tests__/test-message.mjs +0 -190
  36. package/src/cli/install.ts +0 -195
  37. package/src/cli/package-info.ts +0 -48
  38. package/src/cli/preset.ts +0 -54
  39. package/src/cli/qrcode-terminal.d.ts +0 -3
  40. package/src/presets/single-soothing.json +0 -3
@@ -1,47 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { pickSoothingReply } from '../soothing.js'
3
-
4
- const CATEGORIES = [
5
- { input: '我今天好难过', label: '难过类' },
6
- { input: '我很生气', label: '生气类' },
7
- { input: '我好累', label: '累/怕类' },
8
- { input: '给我讲个故事', label: '故事类' },
9
- { input: '唱首儿歌', label: '唱歌类' },
10
- { input: '讲个笑话', label: '笑话类' },
11
- { input: '为什么天是蓝的', label: '知识类' },
12
- { input: '然后呢', label: '继续类' },
13
- { input: '你好', label: '打招呼类' },
14
- { input: '晚安', label: '告别类' },
15
- { input: '帮我连接wifi', label: '配网类' },
16
- { input: '帮我查天气', label: '查询类' },
17
- { input: '随便说点什么', label: '兜底' },
18
- ]
19
-
20
- describe('pickSoothingReply', () => {
21
- it.each(CATEGORIES)('$label — returns a non-empty string', ({ input }) => {
22
- const reply = pickSoothingReply(input)
23
- expect(typeof reply).toBe('string')
24
- expect(reply.length).toBeGreaterThan(0)
25
- })
26
-
27
- it.each(CATEGORIES)('$label — never contains 马上', ({ input }) => {
28
- // Run multiple times to cover random selection
29
- for (let i = 0; i < 20; i++) {
30
- expect(pickSoothingReply(input)).not.toContain('马上')
31
- }
32
- })
33
-
34
- it('randomly selects from candidates (not always the same reply)', () => {
35
- const results = new Set(
36
- Array.from({ length: 40 }, () => pickSoothingReply('我今天好难过'))
37
- )
38
- expect(results.size).toBeGreaterThan(1)
39
- })
40
-
41
- it('告别类 returns only short affirmatives', () => {
42
- const allowed = new Set(['好嘞~', '嗯嗯~', '好的~', '哦~', '嗯~'])
43
- for (let i = 0; i < 20; i++) {
44
- expect(allowed.has(pickSoothingReply('晚安'))).toBe(true)
45
- }
46
- })
47
- })
@@ -1,190 +0,0 @@
1
- /**
2
- * MQTT integration test — simulates a FoloToy toy.
3
- *
4
- * Modes:
5
- * chat — send a message, verify outbound reply format (soothing + reply + finish)
6
- * reminder — send a reminder request, verify reply, wait for notification delivery
7
- *
8
- * Configuration (via .env or environment variables):
9
- * FOLOTOY_TOY_SN — toy serial number
10
- * FOLOTOY_TOY_KEY — toy key (MQTT password)
11
- * FOLOTOY_MQTT_HOST — MQTT broker host
12
- * FOLOTOY_MQTT_PORT — MQTT broker port (default 1883)
13
- * FOLOTOY_TEST_MQTT_USERNAME — MQTT username for test client (default: FOLOTOY_TOY_SN)
14
- * FOLOTOY_TEST_MQTT_PASSWORD — MQTT password for test client (default: FOLOTOY_TOY_KEY)
15
- *
16
- * Usage:
17
- * node --env-file=.env test-mqtt.mjs chat "你好"
18
- * node --env-file=.env test-mqtt.mjs reminder
19
- * node --env-file=.env test-mqtt.mjs # defaults to: chat "你好"
20
- */
21
- import mqtt from 'mqtt'
22
-
23
- const SN = process.env.FOLOTOY_TOY_SN
24
- const KEY = process.env.FOLOTOY_TOY_KEY
25
- const HOST = process.env.FOLOTOY_MQTT_HOST
26
- const PORT = Number(process.env.FOLOTOY_MQTT_PORT || 1883)
27
-
28
- if (!SN || !HOST) {
29
- console.error('Missing FOLOTOY_TOY_SN or FOLOTOY_MQTT_HOST. Set them in .env or as env vars.')
30
- process.exit(1)
31
- }
32
-
33
- // Parse args
34
- const args = process.argv.slice(2)
35
- let mode = 'chat'
36
- let message = '你好'
37
-
38
- if (args[0] === 'chat') {
39
- mode = 'chat'
40
- message = args[1] || '你好'
41
- } else if (args[0] === 'reminder') {
42
- mode = 'reminder'
43
- message = args[1] || '1分钟后提醒我站起来'
44
- } else if (args[0]) {
45
- message = args[0]
46
- }
47
-
48
- const inboundTopic = `/openapi/folotoy/${SN}/thing/command/call`
49
- const outboundTopic = `/openapi/folotoy/${SN}/thing/command/callAck`
50
- const notificationTopic = `/openapi/folotoy/${SN}/thing/event/post`
51
-
52
- // ── Validation state ──
53
- const errors = []
54
- let gotSoothing = false
55
- let gotReply = false
56
- let gotFinish = false
57
- let gotNotification = false
58
-
59
- function validateOutboundMessage(msg) {
60
- if (msg.identifier !== 'chat_output') {
61
- errors.push(`Expected identifier "chat_output", got "${msg.identifier}"`)
62
- }
63
- const p = msg.outParams
64
- if (typeof p?.recording_id !== 'number') {
65
- errors.push(`Missing or invalid recording_id in outbound message (order=${p?.order})`)
66
- }
67
- if (typeof p?.order !== 'number') {
68
- errors.push('Missing or invalid order in outbound message')
69
- }
70
- if (typeof p?.is_finished !== 'boolean') {
71
- errors.push(`Missing or invalid is_finished in outbound message (order=${p?.order})`)
72
- }
73
- }
74
-
75
- function validateNotificationMessage(msg) {
76
- if (msg.identifier !== 'send_notification') {
77
- errors.push(`Expected notification identifier "send_notification", got "${msg.identifier}"`)
78
- }
79
- if (typeof msg.outParams?.text !== 'string' || !msg.outParams.text) {
80
- errors.push('Notification missing outParams.text')
81
- }
82
- if ('recording_id' in (msg.outParams || {})) {
83
- errors.push('Notification should not contain recording_id')
84
- }
85
- }
86
-
87
- function printSummary() {
88
- console.log('\n' + '='.repeat(60))
89
- console.log(`TEST SUMMARY (mode: ${mode})`)
90
- console.log('='.repeat(60))
91
-
92
- const checks = [
93
- ['Soothing ack (order=1)', gotSoothing],
94
- ['AI reply (order=2+)', gotReply],
95
- ['Finish message (is_finished=true)', gotFinish],
96
- ]
97
-
98
- for (const [label, ok] of checks) {
99
- console.log(` ${ok ? '✅' : '❌'} ${label}`)
100
- }
101
-
102
- if (mode === 'reminder') {
103
- console.log(` ${gotNotification ? '✅' : 'ℹ️ '} Notification on event/post${gotNotification ? '' : ' (not received within timeout — may arrive later)'}`)
104
- }
105
-
106
- if (errors.length > 0) {
107
- console.log('\nErrors:')
108
- for (const e of errors) console.log(` ❌ ${e}`)
109
- }
110
-
111
- const allPassed = checks.every(([, ok]) => ok) && errors.length === 0
112
- console.log(`\n${allPassed ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}`)
113
- return allPassed
114
- }
115
-
116
- function done() {
117
- const passed = printSummary()
118
- client.end()
119
- process.exit(passed ? 0 : 1)
120
- }
121
-
122
- // ── MQTT ──
123
- const MQTT_USERNAME = process.env.FOLOTOY_TEST_MQTT_USERNAME || SN
124
- const MQTT_PASSWORD = process.env.FOLOTOY_TEST_MQTT_PASSWORD || KEY
125
-
126
- const client = mqtt.connect(`mqtt://${HOST}:${PORT}`, {
127
- clientId: `test-toy-${Date.now()}`,
128
- username: MQTT_USERNAME,
129
- password: MQTT_PASSWORD,
130
- })
131
-
132
- client.on('connect', () => {
133
- console.log(`[connected] ${HOST}:${PORT} as ${SN}`)
134
- console.log(`[mode] ${mode}`)
135
- console.log(`[message] ${message}`)
136
-
137
- client.subscribe([outboundTopic, notificationTopic], (err) => {
138
- if (err) {
139
- console.error('[subscribe error]', err.message)
140
- process.exit(1)
141
- }
142
-
143
- const msg = {
144
- msgId: 1,
145
- identifier: 'chat_input',
146
- inputParams: { text: message, recording_id: 999 },
147
- }
148
- console.log(`\n[publish] ${inboundTopic}`)
149
- console.log(JSON.stringify(msg, null, 2))
150
- client.publish(inboundTopic, JSON.stringify(msg))
151
- })
152
- })
153
-
154
- client.on('message', (topic, payload) => {
155
- let msg
156
- try { msg = JSON.parse(payload.toString()) } catch { return }
157
-
158
- if (topic === outboundTopic) {
159
- const p = msg.outParams
160
- const label = p?.is_finished ? 'FINISH' : `REPLY(order=${p?.order})`
161
- console.log(`\n[OUTBOUND ${label}] ${p?.content || '(empty)'}`)
162
- validateOutboundMessage(msg)
163
-
164
- if (p?.order === 1 && !p?.is_finished) gotSoothing = true
165
- if (p?.order >= 2 && !p?.is_finished && p?.content) gotReply = true
166
- if (p?.is_finished) {
167
- gotFinish = true
168
- if (mode === 'chat') {
169
- setTimeout(done, 500)
170
- } else {
171
- console.log('\n[waiting up to 90s for notification on event/post topic...]')
172
- }
173
- }
174
- }
175
-
176
- if (topic === notificationTopic) {
177
- console.log(`\n[NOTIFICATION] ${msg.outParams?.text || JSON.stringify(msg)}`)
178
- validateNotificationMessage(msg)
179
- gotNotification = true
180
- setTimeout(done, 500)
181
- }
182
- })
183
-
184
- client.on('error', (err) => {
185
- console.error('[mqtt error]', err.message)
186
- })
187
-
188
- // Timeout: chat 30s, reminder 120s
189
- const timeoutMs = mode === 'reminder' ? 120_000 : 30_000
190
- setTimeout(done, timeoutMs)
@@ -1,195 +0,0 @@
1
- import { execSync } from 'node:child_process'
2
- import qrcode from 'qrcode-terminal'
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'
6
-
7
- const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn'
8
- const POLL_INTERVAL_MS = 3000
9
- const POLL_TIMEOUT_MS = 300_000 // 5 minutes
10
-
11
- // ── Types ──────────────────────────────────────────────
12
-
13
- type CreateSessionResponse = {
14
- session_id: string
15
- pair_url: string
16
- expires_at: string
17
- }
18
-
19
- type PollResponse =
20
- | { status: 'pending' }
21
- | { status: 'completed'; toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }
22
- | { status: 'expired' }
23
-
24
- // ── Helpers ────────────────────────────────────────────
25
-
26
- function checkOpenClaw(): void {
27
- try {
28
- execSync('openclaw --version', { stdio: 'pipe' })
29
- } catch {
30
- console.error('Error: openclaw is not installed or not in PATH.')
31
- console.error('Install it first: npm i -g openclaw')
32
- process.exit(1)
33
- }
34
- }
35
-
36
- function installPlugin(): void {
37
- try {
38
- const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString()
39
- if (list.includes('folotoy-openclaw-plugin')) {
40
- return // already installed
41
- }
42
- } catch {
43
- // ignore
44
- }
45
- const spec = getInstallSpec()
46
- console.log(`Installing FoloToy plugin (${spec})...`)
47
- execSync(`openclaw plugins install ${spec}`, { stdio: 'inherit' })
48
- }
49
-
50
- async function createSession(): Promise<CreateSessionResponse> {
51
- const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' })
52
- if (!res.ok) throw new Error(`Failed to create pairing session (HTTP ${res.status})`)
53
- return res.json() as Promise<CreateSessionResponse>
54
- }
55
-
56
- function displayQR(url: string): void {
57
- qrcode.generate(url, { small: true }, (qr: string) => {
58
- console.log(qr)
59
- })
60
- console.log(`Or open this URL on your phone: ${url}\n`)
61
- }
62
-
63
- async function sleep(ms: number): Promise<void> {
64
- return new Promise((r) => setTimeout(r, ms))
65
- }
66
-
67
- async function pollSession(sessionId: string): Promise<PollResponse & { status: 'completed' }> {
68
- const deadline = Date.now() + POLL_TIMEOUT_MS
69
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
70
- let i = 0
71
-
72
- while (Date.now() < deadline) {
73
- process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`)
74
-
75
- const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`)
76
- if (!res.ok) throw new Error(`Poll failed (HTTP ${res.status})`)
77
- const data = (await res.json()) as PollResponse
78
-
79
- if (data.status === 'completed') {
80
- process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n')
81
- return data as PollResponse & { status: 'completed' }
82
- }
83
- if (data.status === 'expired') {
84
- process.stdout.write('\r')
85
- throw new Error('Pairing session expired. Please try again.')
86
- }
87
-
88
- await sleep(POLL_INTERVAL_MS)
89
- }
90
- throw new Error('Pairing timed out after 5 minutes.')
91
- }
92
-
93
- function restartGateway(): void {
94
- try {
95
- execSync('openclaw gateway restart', { stdio: 'inherit' })
96
- } catch {
97
- console.warn('⚠ Failed to restart gateway. You can restart manually: openclaw gateway restart')
98
- }
99
- }
100
-
101
- function writeConfig(
102
- result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number },
103
- preset: Preset = {},
104
- ): void {
105
- execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
106
- execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
107
- execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' })
108
-
109
- const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST
110
- const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT
111
- execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' })
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
134
- }
135
-
136
- // ── Main ───────────────────────────────────────────────
137
-
138
- async function main() {
139
- const argv = process.argv.slice(2)
140
- const command = argv[0]
141
-
142
- if (command !== 'install') {
143
- const presets = listPresets()
144
- console.log(`Usage: npx ${getPluginName()} install [--preset <name>]`)
145
- if (presets.length) console.log(`Available presets: ${presets.join(', ')}`)
146
- process.exit(command ? 1 : 0)
147
- }
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
-
154
- console.log('🧸 FoloToy OpenClaw Plugin Installer\n')
155
- if (presetName) {
156
- console.log(`Using preset: ${presetName} → ${JSON.stringify(preset)}\n`)
157
- }
158
-
159
- // Step 1: check prerequisites
160
- console.log('Checking openclaw...')
161
- checkOpenClaw()
162
- console.log('✓ openclaw found\n')
163
-
164
- // Step 2: install plugin if not present
165
- installPlugin()
166
-
167
- // Step 3: create pairing session
168
- console.log('Creating pairing session...\n')
169
- const session = await createSession()
170
-
171
- // Step 4: display QR code
172
- console.log('Scan this QR code with your phone,')
173
- console.log('then scan your toy\'s QR code on the phone:\n')
174
- displayQR(session.pair_url)
175
-
176
- // Step 5: poll for result
177
- const result = await pollSession(session.session_id)
178
-
179
- // Step 6: write config
180
- console.log('\nWriting configuration...')
181
- writeConfig(result, preset)
182
-
183
- // Step 7: restart gateway
184
- console.log('\nRestarting gateway...')
185
- restartGateway()
186
-
187
- // Step 8: done
188
- console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m')
189
- console.log(` Toy SN: ${result.toy_sn}`)
190
- }
191
-
192
- main().catch((err) => {
193
- console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`)
194
- process.exit(1)
195
- })
@@ -1,48 +0,0 @@
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
- }
package/src/cli/preset.ts DELETED
@@ -1,54 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- declare module 'qrcode-terminal' {
2
- export function generate(text: string, opts?: { small?: boolean }, cb?: (qr: string) => void): void
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "soothing_loop_enabled": false
3
- }