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.
Files changed (3) hide show
  1. package/lib/client.js +21 -9
  2. package/lib/index.js +147 -68
  3. 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 (projection key + pure appearance renderer). The
11
- // renderer maps the 'caveman' projection value to badge appearance, or
12
- // null when the mode is off. Functionality (projection unit, /caveman
13
- // command, prompt-section injection) lives in the host half.
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
- projection: "caveman",
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
- if (!sessionId) return;
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
- }).catch(() => {});
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
- /* fetch unavailable: no-op */
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
- // - sessionProjections unit 'caveman' folds `caveman/change` session events
3
- // (default 'full'; per-session by construction, no cross-agent sharing)
4
- // - /caveman command and plain-text "caveman <mode>" directive (agent/pre-step)
5
- // append the change event; directive-only user messages are consumed
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 via
8
- // useProjection('caveman')
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
- // OUT-OF-REPO EVENT VOCABULARY:
11
- // `caveman/change` is not in the harness's static KNOWN_SESSION_EVENT_TYPES
12
- // (generated in @deepseek-ai/dsh-session), so a session log containing it is
13
- // refused on load unless the type is registered or the event carries
14
- // `ignorable: true` (append() cannot set that flag). We register the type at
15
- // apply() time on the SAME module instance the persistence loader imports
16
- // (both resolve to <workspace>/node_modules/@deepseek-ai/dsh-session), so
17
- // every boot WITH this plugin can read back its events. Logs written before
18
- // this fix were repaired in place by .dsh/scripts/repair-caveman-events.py /
19
- // fix-d8b43aa8.py (ignorable: true injected, 2-frame zstd layout).
20
- import { z } from 'zod'
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
- // Register our event type on the shared KNOWN set so session logs
54
- // containing `caveman/change` load in any boot that mounts this plugin.
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
- KNOWN_SESSION_EVENT_TYPES.add('caveman/change')
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
- const projections = ctx.get('sessionProjections')
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
- if (projections !== undefined) {
64
- // rc.7+: client visibility requires wire (viewSchema + view); the bare
65
- // top-level schema/view fields are ignored by the rc.2 registry.
66
- projections.register({
67
- key: 'caveman',
68
- stateSchema: schema,
69
- stateVersion: 1,
70
- init: () => ({ mode: 'full' }),
71
- apply: (state, event) => {
72
- if (!event || event.type !== 'caveman/change') return state
73
- const mode = event.data && typeof event.data.mode === 'string' ? event.data.mode : ''
74
- if (!VALID.includes(mode) || state.mode === mode) return state
75
- return { mode }
76
- },
77
- wire: {
78
- viewSchema: schema,
79
- view: (state) => ({ mode: state.mode }),
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.append('caveman/change', { mode })
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.append('caveman/change', { mode })
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 click toggle: POST /caveman/toggle { sessionId } -------------
117
- // The composer badge's onClick cycles the mode off -> lite -> full -> ultra
118
- // -> off through the same caveman/change event path as /caveman on|off, so
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
- let mode = 'full'
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
- session.append('caveman/change', { mode: next })
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 || projections === undefined) return ''
183
- let mode = 'full'
184
- try {
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 (event projection + KNOWN registration)')
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.2",
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 (session projection unit) + composer dock badge + /caveman command + prompt-section injection. Caveman skills ship from the deployment shared skill library (see ~/.dsh/skills), not from this package.",
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"