dsh-wisp 0.6.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/LICENSE +32 -0
- package/NOTICE.md +151 -0
- package/README.md +285 -0
- package/assets/happy.webp +0 -0
- package/assets/happy_hi.webp +0 -0
- package/assets/happy_md.webp +0 -0
- package/assets/happy_sm.webp +0 -0
- package/assets/idle.webp +0 -0
- package/assets/idle_hi.webp +0 -0
- package/assets/idle_md.webp +0 -0
- package/assets/idle_sm.webp +0 -0
- package/assets/sleepy.webp +0 -0
- package/assets/sleepy_hi.webp +0 -0
- package/assets/sleepy_md.webp +0 -0
- package/assets/sleepy_sm.webp +0 -0
- package/assets/work.webp +0 -0
- package/assets/work_hi.webp +0 -0
- package/assets/work_md.webp +0 -0
- package/assets/work_sm.webp +0 -0
- package/build.mjs +169 -0
- package/cordis.patch.yml +51 -0
- package/lib/client.js +1091 -0
- package/lib/client.template.js +1086 -0
- package/lib/index.js +19 -0
- package/package.json +72 -0
- package/tools/CHARACTER-PROMPT.md +88 -0
- package/tools/assets.mjs +139 -0
- package/tools/keyout.mjs +104 -0
- package/verify-wisp.mjs +872 -0
package/build.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
build.mjs — inline the sprites into lib/client.js.
|
|
3
|
+
|
|
4
|
+
lib/client.template.js holds the whole browser half with one placeholder,
|
|
5
|
+
__SPRITES__. This script replaces it with a small object of data URIs:
|
|
6
|
+
|
|
7
|
+
{ idle: "data:image/webp;base64,…", happy: …, sleepy: …, work: … }
|
|
8
|
+
|
|
9
|
+
Embedding rather than shipping asset files is deliberate: the delivered
|
|
10
|
+
plugin is a single self-contained file, so a share carries no path, no
|
|
11
|
+
extra route, and no missing-asset failure mode.
|
|
12
|
+
|
|
13
|
+
RUN
|
|
14
|
+
node build.mjs # default tier: 1024x1536
|
|
15
|
+
node build.mjs --tier=hi # 2048x3072 masters (~3x the bundle)
|
|
16
|
+
node build.mjs --tier=md # 512x768
|
|
17
|
+
node build.mjs --tier=sm # 256x384 (--small is an alias)
|
|
18
|
+
node build.mjs --from assets/ # optional, defaults to ./assets
|
|
19
|
+
|
|
20
|
+
WHICH SOURCE IS INLINED MATTERS FOR SHARPNESS. assets/ carries four tiers per
|
|
21
|
+
mood, all downsampled from one 2048x3072 master:
|
|
22
|
+
|
|
23
|
+
<mood>.webp 1024x1536 default sharp to devicePixelRatio ~4.9 at the
|
|
24
|
+
shipped render size (210x315 CSS px)
|
|
25
|
+
<mood>_md.webp 512x768 the pre-0.4 size
|
|
26
|
+
<mood>_sm.webp 256x384 smallest
|
|
27
|
+
<mood>_hi.webp 2048x3072 headroom for a much larger wisp
|
|
28
|
+
|
|
29
|
+
The default is the smallest tier that is sharp on every realistic display; the
|
|
30
|
+
tiers above it add bytes, not visible pixels, until she is drawn much bigger.
|
|
31
|
+
========================================================================== */
|
|
32
|
+
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
import { dirname, join, resolve } from 'node:path'
|
|
36
|
+
|
|
37
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
38
|
+
const argv = process.argv.slice(2)
|
|
39
|
+
const flagValue = (name, fallback) => {
|
|
40
|
+
const at = argv.indexOf(name)
|
|
41
|
+
if (at >= 0 && argv[at + 1] !== undefined) return argv[at + 1]
|
|
42
|
+
const inline = argv.find((a) => a.startsWith(`${name}=`))
|
|
43
|
+
return inline ? inline.slice(name.length + 1) : fallback
|
|
44
|
+
}
|
|
45
|
+
const assetsDir = resolve(here, flagValue('--from', 'assets'))
|
|
46
|
+
const templatePath = join(here, 'lib', 'client.template.js')
|
|
47
|
+
const outPath = join(here, 'lib', 'client.js')
|
|
48
|
+
|
|
49
|
+
const MOODS = ['idle', 'happy', 'sleepy', 'work']
|
|
50
|
+
|
|
51
|
+
/** Preference order per tier: the first hit wins, later entries are fallbacks. */
|
|
52
|
+
const TIERS = {
|
|
53
|
+
hi: ['_hi', '', '_md', '_sm'],
|
|
54
|
+
full: ['', '_hi', '_md', '_sm'],
|
|
55
|
+
md: ['_md', '', '_sm'],
|
|
56
|
+
sm: ['_sm', '_md', ''],
|
|
57
|
+
}
|
|
58
|
+
const alias = argv.includes('--small') ? 'sm' : argv.includes('--hi') ? 'hi' : argv.includes('--md') ? 'md' : 'full'
|
|
59
|
+
const tier = flagValue('--tier', alias)
|
|
60
|
+
if (TIERS[tier] === undefined) {
|
|
61
|
+
console.error(`build: unknown --tier "${tier}" (expected one of ${Object.keys(TIERS).join(', ')})`)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!existsSync(assetsDir)) {
|
|
66
|
+
console.error(`build: assets directory not found: ${assetsDir}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pickFile = (mood) => {
|
|
71
|
+
for (const suffix of TIERS[tier]) {
|
|
72
|
+
for (const ext of ['.webp', '.png']) {
|
|
73
|
+
const p = join(assetsDir, `${mood}${suffix}${ext}`)
|
|
74
|
+
if (existsSync(p)) return p
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sprites = {}
|
|
81
|
+
let total = 0
|
|
82
|
+
for (const mood of MOODS) {
|
|
83
|
+
const file = pickFile(mood)
|
|
84
|
+
if (!file) {
|
|
85
|
+
console.error(`build: no sprite for mood "${mood}" in ${assetsDir}`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
const bytes = readFileSync(file)
|
|
89
|
+
total += bytes.length
|
|
90
|
+
const ext = file.slice(file.lastIndexOf('.') + 1).toLowerCase()
|
|
91
|
+
const mime = ext === 'png' ? 'image/png' : 'image/webp'
|
|
92
|
+
sprites[mood] = `data:${mime};base64,${bytes.toString('base64')}`
|
|
93
|
+
console.log(` ${mood.padEnd(7)} ${String(Math.round(bytes.length / 1024)).padStart(4)} KB <- ${file.slice(here.length + 1)}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const PLACEHOLDER = '__SPRITES_LITERAL__'
|
|
97
|
+
const template = readFileSync(templatePath, 'utf8')
|
|
98
|
+
const placeholderCount = template.split(PLACEHOLDER).length - 1
|
|
99
|
+
if (placeholderCount !== 1) {
|
|
100
|
+
// Exactly one occurrence, and it must NOT sit inside a comment. An earlier
|
|
101
|
+
// revision used `/*__SPRITES__*/{}` as the placeholder; the substitution then
|
|
102
|
+
// left `/*{…}`, the whole injected table became a block comment, SPRITES
|
|
103
|
+
// silently evaluated to {}, and every sprite rendered as a broken image.
|
|
104
|
+
console.error(`build: template must contain ${PLACEHOLDER} exactly once (found ${placeholderCount})`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const literal = '{\n' + MOODS.map((m) => ` ${m}: '${sprites[m]}',`).join('\n') + '\n }'
|
|
109
|
+
const out = template.replace(PLACEHOLDER, literal)
|
|
110
|
+
|
|
111
|
+
if (out.includes(PLACEHOLDER)) {
|
|
112
|
+
console.error('build: placeholder survived the substitution; refusing to write a broken bundle')
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---- guard: never call a global the dynamic client runner traps ----------
|
|
117
|
+
setTimeout/setInterval/clearTimeout/clearInterval/fetch/require are shadowed
|
|
118
|
+
by THROWING parameters in a client half, so a call to one is a guaranteed
|
|
119
|
+
runtime crash that no bundler reports. The pattern demands a real call, so
|
|
120
|
+
the prose in the file header (which names them without parentheses) is not a
|
|
121
|
+
false positive. verify-wisp.mjs proves the same thing by executing the bundle
|
|
122
|
+
with all six shadowed by traps. */
|
|
123
|
+
const TRAPPED_CALL = /(setTimeout|setInterval|clearTimeout|clearInterval|fetch|require)\s*\(/
|
|
124
|
+
const trapped = out.match(TRAPPED_CALL)
|
|
125
|
+
if (trapped) {
|
|
126
|
+
console.error(`build: the bundle calls the trapped global "${trapped[1]}" — the client half would throw at runtime`)
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ---- guard: the stamped version must match package.json ------------------ */
|
|
131
|
+
const pkg = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'))
|
|
132
|
+
const stamped = out.match(/const VERSION = '([^']+)'/)
|
|
133
|
+
if (!stamped) {
|
|
134
|
+
console.error('build: client half carries no VERSION constant; the mount log would lie about the running build')
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
if (stamped[1] !== pkg.version) {
|
|
138
|
+
console.error(`build: version drift — client stamps ${stamped[1]}, package.json says ${pkg.version}`)
|
|
139
|
+
process.exit(1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Prove the injected table survives parsing before writing anything.
|
|
143
|
+
const table = out.match(/const SPRITES = \{([\s\S]*?)\n {4}\}/)
|
|
144
|
+
if (!table || MOODS.some((m) => !new RegExp(`\\b${m}:\\s*'data:image/`).test(table[1]))) {
|
|
145
|
+
console.error('build: the injected SPRITES table is missing sprites; aborting')
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// Evaluate exactly that statement to be certain it is not commented out.
|
|
150
|
+
const value = new Function(`${table[0]}\nreturn SPRITES`)()
|
|
151
|
+
if (MOODS.some((m) => typeof value[m] !== 'string' || value[m].indexOf('data:image/') !== 0)) {
|
|
152
|
+
console.error(`build: SPRITES evaluated to ${JSON.stringify(Object.keys(value))}; aborting`)
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`build: injected SPRITES table does not evaluate (${error.message}); aborting`)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeFileSync(outPath, out, 'utf8')
|
|
161
|
+
|
|
162
|
+
const kb = (n) => (n / 1024).toFixed(1)
|
|
163
|
+
console.log(`\n sprites total ${kb(total)} KB`)
|
|
164
|
+
console.log(` base64 inlined ${kb(out.length)} KB`)
|
|
165
|
+
console.log(` wrote lib/client.js`)
|
|
166
|
+
|
|
167
|
+
if (assetsDir !== join(here, 'assets') && readdirSync(assetsDir).length === 0) {
|
|
168
|
+
console.warn('\n warning: assets directory is empty')
|
|
169
|
+
}
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Bundle patch for dsh-wisp.
|
|
2
|
+
#
|
|
3
|
+
# A profile points at this file through its own `dsh.bundle.patch` field; the
|
|
4
|
+
# loader reads it as a top-level YAML array of patch entries, exactly like the
|
|
5
|
+
# shipped bundles do.
|
|
6
|
+
#
|
|
7
|
+
# THE ROW BELOW POINTS AT THE HOST HALF — `dsh-wisp`, not `dsh-wisp/client`.
|
|
8
|
+
#
|
|
9
|
+
# This distinction is the whole reason the plugin failed to load three times.
|
|
10
|
+
# The loader imports this name as an ES module. `dsh-wisp` resolves to
|
|
11
|
+
# lib/index.js, which is a valid ES module. `dsh-wisp/client` resolves to
|
|
12
|
+
# lib/client.js, which is a classic browser script beginning with
|
|
13
|
+
# `window.__ModuleLoader__.load(...)` — the host cannot import that, so
|
|
14
|
+
# `entry.fiber` stayed undefined and @deepseek-ai/dsh-app-boot reported its
|
|
15
|
+
# terse `"failed to import"`.
|
|
16
|
+
#
|
|
17
|
+
# The browser half still ships: it is discovered through package.json
|
|
18
|
+
# `exports["./client"]` (dsh-client-modules reads that, it does not import it)
|
|
19
|
+
# and delivered to the page by the /plugins bundle route.
|
|
20
|
+
#
|
|
21
|
+
# Config keys the client half accepts — every one optional, every one clamped to
|
|
22
|
+
# a sane range, so a typo degrades to the default instead of leaving an
|
|
23
|
+
# unusable companion on screen:
|
|
24
|
+
# right distance from the right edge in px (default 26)
|
|
25
|
+
# bottom distance from the bottom edge in px (default 46)
|
|
26
|
+
# size render scale, 0.4–8 (default 4)
|
|
27
|
+
# -> drawn 560x840 CSS px from the 2048x3072 sprite tier
|
|
28
|
+
# sleepAfterMs idle time before dozing off in ms (default 90000)
|
|
29
|
+
# reactions react to agent-busy / composer text (default true)
|
|
30
|
+
# persist remember where she was dragged to (default true)
|
|
31
|
+
# celebrate cheer when a run finishes (default true)
|
|
32
|
+
# celebrateAfterMs only celebrate runs at least this long (default 2500)
|
|
33
|
+
# happyMs how long the happy state lasts (default 1100)
|
|
34
|
+
# chatterMs idle line interval; 0 = stay quiet (default 0)
|
|
35
|
+
# wander stroll around on her own while idle (default true)
|
|
36
|
+
# wanderMs how often she considers a stroll (default 45000)
|
|
37
|
+
# wanderRange how far one stroll may travel, in px (default 260)
|
|
38
|
+
#
|
|
39
|
+
# Example — add a `config:` block to the row below:
|
|
40
|
+
#
|
|
41
|
+
# - insert:
|
|
42
|
+
# - id: wisp
|
|
43
|
+
# name: "dsh-wisp"
|
|
44
|
+
# config:
|
|
45
|
+
# bottom: 90
|
|
46
|
+
# size: 1.2
|
|
47
|
+
# chatterMs: 300000
|
|
48
|
+
|
|
49
|
+
- insert:
|
|
50
|
+
- id: wisp
|
|
51
|
+
name: "dsh-wisp"
|