dsh-caveman 0.1.2 → 0.1.4
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/lib/client.js +21 -9
- package/lib/index.js +147 -68
- package/package.json +2 -5
package/lib/client.js
CHANGED
|
@@ -7,10 +7,10 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
const React = require("react");
|
|
8
8
|
|
|
9
9
|
// Badge display is centralized in dsh-badges: this plugin only registers
|
|
10
|
-
// a badge descriptor (
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// a badge descriptor (fetch-mode: initial GET + toggle-response self-
|
|
11
|
+
// update + 5s re-sync). No session projection is involved; per-session
|
|
12
|
+
// mode lives in the host sidecar. Functionality (/caveman command,
|
|
13
|
+
// directive parsing, prompt-section injection) lives in the host half.
|
|
14
14
|
const inject = ["slots"];
|
|
15
15
|
|
|
16
16
|
function apply(ctx) {
|
|
@@ -24,18 +24,30 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
if (registry === undefined) return false;
|
|
25
25
|
registry.register({
|
|
26
26
|
key: "caveman",
|
|
27
|
-
|
|
27
|
+
fetch: (sessionId) =>
|
|
28
|
+
sessionId
|
|
29
|
+
? fetch("/caveman/state?sessionId=" + encodeURIComponent(sessionId))
|
|
30
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
31
|
+
.catch(() => null)
|
|
32
|
+
: Promise.resolve(null),
|
|
33
|
+
pollMs: 5000,
|
|
28
34
|
title: "Click to cycle caveman mode: off -> lite -> full -> ultra",
|
|
29
35
|
onClick: (sessionId) => {
|
|
30
|
-
|
|
36
|
+
// Cycles through the host sidecar (POST /caveman/toggle). The
|
|
37
|
+
// resolved value updates the badge immediately (no waiting for
|
|
38
|
+
// the next poll). Failures are silent (badge stays as-is).
|
|
39
|
+
if (!sessionId) return Promise.resolve(null);
|
|
31
40
|
try {
|
|
32
|
-
fetch("/caveman/toggle", {
|
|
41
|
+
return fetch("/caveman/toggle", {
|
|
33
42
|
method: "POST",
|
|
34
43
|
headers: { "content-type": "application/json" },
|
|
35
44
|
body: JSON.stringify({ sessionId }),
|
|
36
|
-
})
|
|
45
|
+
})
|
|
46
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
47
|
+
.then((data) => (data && data.ok ? { mode: data.mode } : null))
|
|
48
|
+
.catch(() => null);
|
|
37
49
|
} catch (e) {
|
|
38
|
-
|
|
50
|
+
return Promise.resolve(null);
|
|
39
51
|
}
|
|
40
52
|
},
|
|
41
53
|
render: (value) => {
|
package/lib/index.js
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
// dsh-caveman: per-session caveman communication mode.
|
|
2
|
-
// -
|
|
3
|
-
// (default 'full';
|
|
4
|
-
// - /caveman command
|
|
5
|
-
//
|
|
2
|
+
// - per-session mode state lives in a sidecar JSON keyed by session id
|
|
3
|
+
// (unseen sessions use the settings default 'full'; no cross-agent sharing)
|
|
4
|
+
// - /caveman command, plain-text "caveman <mode>" directive (agent/pre-step),
|
|
5
|
+
// and POST /caveman/toggle all flip the sidecar state; directive-only user
|
|
6
|
+
// messages are still consumed
|
|
6
7
|
// - system-prompt section injects the current mode as an overriding directive
|
|
7
|
-
// - composer dock badge (client half, lib/client.js) renders
|
|
8
|
-
//
|
|
8
|
+
// - composer dock badge (client half, lib/client.js) renders in fetch mode
|
|
9
|
+
// (GET /caveman/state + toggle response self-update + 5s re-sync)
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
11
|
+
// NO-SESSION-EVENT DESIGN (a' refactor 2026-09-08):
|
|
12
|
+
// this plugin WRITES zero session events. The KNOWN registration (dual-path,
|
|
13
|
+
// below) is KEPT as read-side legacy compat only: rc.2 persistence refuses to
|
|
14
|
+
// load logs containing unregistered, non-ignorable event types
|
|
15
|
+
// (dsh-session-persistence L1120), and historic sessions contain
|
|
16
|
+
// `caveman/change` (实撞 2026-09-08 via rtk: dropping the registration breaks
|
|
17
|
+
// legacy log loading). Logs written before the old fix were repaired in place
|
|
18
|
+
// by .dsh/scripts/repair-caveman-events.py / fix-d8b43aa8.py.
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
21
22
|
import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
|
|
23
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
24
|
+
// NOTE: no `settingsNamespace` import — the brand helper was removed from
|
|
25
|
+
// dsh-settings@0.1.2+ (see dsh-rtk for the same fix). Namespace is a literal.
|
|
26
|
+
import zs from '@deepseek-ai/schemastery'
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url)
|
|
29
|
+
|
|
30
|
+
function registerKnownEventType(type) {
|
|
31
|
+
KNOWN_SESSION_EVENT_TYPES.add(type)
|
|
32
|
+
try {
|
|
33
|
+
const persistencePath = require.resolve('@deepseek-ai/dsh-session-persistence')
|
|
34
|
+
const hostSessionPath = require.resolve('@deepseek-ai/dsh-session', { paths: [persistencePath] })
|
|
35
|
+
require(hostSessionPath).KNOWN_SESSION_EVENT_TYPES.add(type)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('[caveman] could not register host session event type:', err && err.message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
|
|
23
41
|
export const name = 'caveman'
|
|
24
42
|
// Hard dependency on the HTTP route registry for the badge-click toggle
|
|
@@ -29,8 +47,6 @@ const VALID = ['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'we
|
|
|
29
47
|
const DIRECTIVE_RE = /^\/?caveman\s+(off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra)$/i
|
|
30
48
|
const OFF_RE = /^(stop\s+caveman|normal\s+mode)$/i
|
|
31
49
|
|
|
32
|
-
const schema = z.object({ mode: z.enum(VALID) })
|
|
33
|
-
|
|
34
50
|
function parseDirective(text) {
|
|
35
51
|
if (typeof text !== 'string') return null
|
|
36
52
|
const t = text.trim()
|
|
@@ -50,36 +66,81 @@ function directiveOf(message) {
|
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
export function apply(ctx) {
|
|
53
|
-
//
|
|
54
|
-
// containing `caveman/change`
|
|
69
|
+
// READ-SIDE LEGACY COMPAT ONLY (do not remove): keeps session logs
|
|
70
|
+
// containing historic `caveman/change` loadable; this plugin no longer
|
|
71
|
+
// WRITES session events (see header). Drop only after SOP §8 injection.
|
|
55
72
|
try {
|
|
56
|
-
|
|
73
|
+
registerKnownEventType('caveman/change')
|
|
57
74
|
} catch (err) {
|
|
58
75
|
console.warn('[caveman] could not register event type:', err && err.message)
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
// ---- global default mode (settings document, 'caveman' namespace) ------
|
|
79
|
+
// Sessions WITHOUT a sidecar entry follow this live default (full).
|
|
80
|
+
let defaultMode = 'full'
|
|
81
|
+
let cavemanScope = null
|
|
82
|
+
const settings = ctx.get('settings')
|
|
83
|
+
if (settings !== undefined) {
|
|
84
|
+
try {
|
|
85
|
+
cavemanScope = settings.register(
|
|
86
|
+
'caveman',
|
|
87
|
+
zs.object({ defaultMode: zs.union(VALID.map((v) => zs.const(v))).default('full') }),
|
|
88
|
+
{ applies: 'live' },
|
|
89
|
+
)
|
|
90
|
+
const value = cavemanScope.get()
|
|
91
|
+
defaultMode = value && VALID.includes(value.defaultMode) ? value.defaultMode : 'full'
|
|
92
|
+
cavemanScope.watch((next) => {
|
|
93
|
+
defaultMode = next && VALID.includes(next.defaultMode) ? next.defaultMode : 'full'
|
|
94
|
+
})
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.warn('[caveman] settings namespace registration failed:', err && err.message)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
// ---- per-session mode state: sidecar JSON keyed by session id ----------
|
|
101
|
+
// Truth source for the mode. Sessions WITHOUT an entry use the live
|
|
102
|
+
// settings default. Atomic tmp+rename persist; lives outside node_modules
|
|
103
|
+
// (survives restarts and plugin reinstalls).
|
|
104
|
+
const SIDECAR_PATH = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return join(dshHomePath('plugin-state'), 'caveman-state.json')
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn('[caveman] dshHomePath unavailable, sidecar persistence disabled:', e && e.message)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
})()
|
|
112
|
+
const sessionMode = new Map()
|
|
113
|
+
const loadSidecar = () => {
|
|
114
|
+
if (SIDECAR_PATH === null) return
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(readFileSync(SIDECAR_PATH, 'utf8'))
|
|
117
|
+
const sessions = data && data.sessions
|
|
118
|
+
if (sessions && typeof sessions === 'object') {
|
|
119
|
+
for (const [id, mode] of Object.entries(sessions)) {
|
|
120
|
+
if (typeof mode === 'string' && VALID.includes(mode)) sessionMode.set(id, mode)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// missing or corrupt sidecar: start from an empty map (defaults apply)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const persistSidecar = () => {
|
|
128
|
+
if (SIDECAR_PATH === null) return
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(dirname(SIDECAR_PATH), { recursive: true })
|
|
131
|
+
const tmp = SIDECAR_PATH + '.tmp'
|
|
132
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(sessionMode) }), 'utf8')
|
|
133
|
+
renameSync(tmp, SIDECAR_PATH)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.warn('[caveman] sidecar persist failed:', e && e.message)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const readMode = (sessionId) => (sessionMode.has(sessionId) ? sessionMode.get(sessionId) : defaultMode)
|
|
139
|
+
const writeMode = (sessionId, mode) => {
|
|
140
|
+
sessionMode.set(sessionId, mode)
|
|
141
|
+
persistSidecar()
|
|
82
142
|
}
|
|
143
|
+
loadSidecar()
|
|
83
144
|
|
|
84
145
|
const commands = ctx.get('commands')
|
|
85
146
|
if (commands !== undefined) {
|
|
@@ -92,7 +153,8 @@ export function apply(ctx) {
|
|
|
92
153
|
if (!VALID.includes(mode)) {
|
|
93
154
|
return { kind: 'error', text: 'Invalid mode: "' + invocation.rawInput.trim() + '". Valid: ' + VALID.join('|') }
|
|
94
155
|
}
|
|
95
|
-
invocation.agent.session
|
|
156
|
+
const sid = invocation.agent && invocation.agent.session && invocation.agent.session.id
|
|
157
|
+
if (typeof sid === 'string' && sid.length > 0) writeMode(sid, mode)
|
|
96
158
|
return { kind: 'success', text: mode === 'off' ? 'Caveman mode: off' : 'Caveman mode: ' + mode }
|
|
97
159
|
},
|
|
98
160
|
})
|
|
@@ -108,17 +170,48 @@ export function apply(ctx) {
|
|
|
108
170
|
else kept.push(m)
|
|
109
171
|
}
|
|
110
172
|
if (mode === null) return next()
|
|
111
|
-
payload.agent.session
|
|
173
|
+
const sid = payload.agent && payload.agent.session && payload.agent.session.id
|
|
174
|
+
if (typeof sid === 'string' && sid.length > 0) writeMode(sid, mode)
|
|
112
175
|
if (kept.length === 0) return { kind: 'reject' }
|
|
113
176
|
return { kind: 'enter', messages: kept }
|
|
114
177
|
})
|
|
115
178
|
|
|
116
|
-
// ---- badge
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// UI, projection, and prompt-section all agree via the session log.
|
|
179
|
+
// ---- badge state read: GET /caveman/state?sessionId=... ----------------
|
|
180
|
+
// Fetch-mode badge client reads the per-session mode here (initial load
|
|
181
|
+
// plus a periodic re-sync). Exact route; the matcher strips the query.
|
|
120
182
|
const CYCLE = ['off', 'lite', 'full', 'ultra']
|
|
121
183
|
const webServer = ctx.get('webServer')
|
|
184
|
+
if (webServer !== undefined) {
|
|
185
|
+
try {
|
|
186
|
+
webServer.register({
|
|
187
|
+
kind: 'exact',
|
|
188
|
+
path: '/caveman/state',
|
|
189
|
+
handler: async (req, res) => {
|
|
190
|
+
if (req.method !== 'GET') {
|
|
191
|
+
res.writeHead(405)
|
|
192
|
+
res.end()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
let sessionId = null
|
|
196
|
+
try {
|
|
197
|
+
sessionId = new URL(req.url || '/', 'http://x').searchParams.get('sessionId')
|
|
198
|
+
} catch (e) {
|
|
199
|
+
sessionId = null
|
|
200
|
+
}
|
|
201
|
+
const mode = typeof sessionId === 'string' && sessionId.length > 0 ? readMode(sessionId) : 'full'
|
|
202
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
203
|
+
res.end(JSON.stringify({ mode }))
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error('[caveman] /caveman/state route registration failed:', e && e.message)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- badge click toggle: POST /caveman/toggle { sessionId } -------------
|
|
212
|
+
// Cycles off -> lite -> full -> ultra -> off through the sidecar (the same
|
|
213
|
+
// mutation /caveman performs) and returns the new value so the badge can
|
|
214
|
+
// self-update from the response.
|
|
122
215
|
if (webServer !== undefined) {
|
|
123
216
|
try {
|
|
124
217
|
webServer.register({
|
|
@@ -151,18 +244,9 @@ export function apply(ctx) {
|
|
|
151
244
|
res.end(JSON.stringify({ ok: false, error: 'session not found' }))
|
|
152
245
|
return
|
|
153
246
|
}
|
|
154
|
-
|
|
155
|
-
if (projections !== undefined) {
|
|
156
|
-
try {
|
|
157
|
-
const value = projections.snapshot(session).values.caveman
|
|
158
|
-
if (value && typeof value.mode === 'string') mode = value.mode
|
|
159
|
-
} catch (e) {
|
|
160
|
-
mode = 'full'
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const at = CYCLE.indexOf(mode)
|
|
247
|
+
const at = CYCLE.indexOf(readMode(sessionId))
|
|
164
248
|
const next = CYCLE[(at === -1 ? 0 : at) + 1] === undefined ? CYCLE[0] : CYCLE[at + 1]
|
|
165
|
-
|
|
249
|
+
writeMode(sessionId, next)
|
|
166
250
|
res.writeHead(200, { 'content-type': 'application/json' })
|
|
167
251
|
res.end(JSON.stringify({ ok: true, mode: next }))
|
|
168
252
|
},
|
|
@@ -179,14 +263,9 @@ export function apply(ctx) {
|
|
|
179
263
|
order: 50,
|
|
180
264
|
text: (context) => {
|
|
181
265
|
const agent = context && context.agent
|
|
182
|
-
if (!agent
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const value = projections.snapshot(agent.session).values.caveman
|
|
186
|
-
if (value && typeof value.mode === 'string') mode = value.mode
|
|
187
|
-
} catch (err) {
|
|
188
|
-
mode = 'full'
|
|
189
|
-
}
|
|
266
|
+
if (!agent) return ''
|
|
267
|
+
const sid = agent.session && agent.session.id
|
|
268
|
+
const mode = typeof sid === 'string' && sid.length > 0 ? readMode(sid) : defaultMode
|
|
190
269
|
if (mode === 'off') return ''
|
|
191
270
|
const tag = mode === 'full' ? '[CAVEMAN]' : '[CAVEMAN:' + mode.toUpperCase() + ']'
|
|
192
271
|
return tag + ' Current user-set communication style. Follow the caveman skill rules at intensity "' + mode + '" for every response; this overrides any default in AGENTS.md.'
|
|
@@ -194,5 +273,5 @@ export function apply(ctx) {
|
|
|
194
273
|
})
|
|
195
274
|
}
|
|
196
275
|
|
|
197
|
-
console.log('[caveman] host active (
|
|
276
|
+
console.log('[caveman] host active (sidecar mode state + prompt section; writes no session events)')
|
|
198
277
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-caveman",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"./client": "./lib/client.js",
|
|
9
9
|
"./package.json": "./package.json"
|
|
10
10
|
},
|
|
11
|
-
"description": "Per-session caveman mode state
|
|
11
|
+
"description": "Per-session caveman mode state + composer dock badge + /caveman command + prompt-section injection. Writes NO session events: mode state lives in a $DSH_HOME/plugin-state sidecar keyed by session id (default from the 'caveman' settings namespace), and the badge reads GET /caveman/state (fetch mode). The KNOWN event-type registration is kept read-only for legacy sessions. Caveman skills ship from the deployment shared skill library (see ~/.dsh/skills), not from this package. Compatible with @deepseek-ai/dsh-settings 0.1.2+: the namespace is passed as a literal string because the settingsNamespace brand helper was removed upstream (broken named import on 0.1.2-rc.1 boot).",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"dsh",
|
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
"lib",
|
|
24
24
|
"cordis.patch.yml"
|
|
25
25
|
],
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"zod": "^4.4.3"
|
|
28
|
-
},
|
|
29
26
|
"dsh": {
|
|
30
27
|
"bundle": {
|
|
31
28
|
"patch": "./cordis.patch.yml"
|