dsh-caveman 0.1.2 → 0.1.3

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 +146 -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,41 @@
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
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
25
+ import zs from '@deepseek-ai/schemastery'
26
+
27
+ const require = createRequire(import.meta.url)
28
+
29
+ function registerKnownEventType(type) {
30
+ KNOWN_SESSION_EVENT_TYPES.add(type)
31
+ try {
32
+ const persistencePath = require.resolve('@deepseek-ai/dsh-session-persistence')
33
+ const hostSessionPath = require.resolve('@deepseek-ai/dsh-session', { paths: [persistencePath] })
34
+ require(hostSessionPath).KNOWN_SESSION_EVENT_TYPES.add(type)
35
+ } catch (err) {
36
+ console.warn('[caveman] could not register host session event type:', err && err.message)
37
+ }
38
+ }
22
39
 
23
40
  export const name = 'caveman'
24
41
  // Hard dependency on the HTTP route registry for the badge-click toggle
@@ -29,8 +46,6 @@ const VALID = ['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'we
29
46
  const DIRECTIVE_RE = /^\/?caveman\s+(off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra)$/i
30
47
  const OFF_RE = /^(stop\s+caveman|normal\s+mode)$/i
31
48
 
32
- const schema = z.object({ mode: z.enum(VALID) })
33
-
34
49
  function parseDirective(text) {
35
50
  if (typeof text !== 'string') return null
36
51
  const t = text.trim()
@@ -50,36 +65,81 @@ function directiveOf(message) {
50
65
  }
51
66
 
52
67
  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.
68
+ // READ-SIDE LEGACY COMPAT ONLY (do not remove): keeps session logs
69
+ // containing historic `caveman/change` loadable; this plugin no longer
70
+ // WRITES session events (see header). Drop only after SOP §8 injection.
55
71
  try {
56
- KNOWN_SESSION_EVENT_TYPES.add('caveman/change')
72
+ registerKnownEventType('caveman/change')
57
73
  } catch (err) {
58
74
  console.warn('[caveman] could not register event type:', err && err.message)
59
75
  }
60
76
 
61
- const projections = ctx.get('sessionProjections')
77
+ // ---- global default mode (settings document, 'caveman' namespace) ------
78
+ // Sessions WITHOUT a sidecar entry follow this live default (full).
79
+ let defaultMode = 'full'
80
+ let cavemanScope = null
81
+ const settings = ctx.get('settings')
82
+ if (settings !== undefined) {
83
+ try {
84
+ cavemanScope = settings.register(
85
+ settingsNamespace('caveman'),
86
+ zs.object({ defaultMode: zs.union(VALID.map((v) => zs.const(v))).default('full') }),
87
+ { applies: 'live' },
88
+ )
89
+ const value = cavemanScope.get()
90
+ defaultMode = value && VALID.includes(value.defaultMode) ? value.defaultMode : 'full'
91
+ cavemanScope.watch((next) => {
92
+ defaultMode = next && VALID.includes(next.defaultMode) ? next.defaultMode : 'full'
93
+ })
94
+ } catch (err) {
95
+ console.warn('[caveman] settings namespace registration failed:', err && err.message)
96
+ }
97
+ }
62
98
 
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
- })
99
+ // ---- per-session mode state: sidecar JSON keyed by session id ----------
100
+ // Truth source for the mode. Sessions WITHOUT an entry use the live
101
+ // settings default. Atomic tmp+rename persist; lives outside node_modules
102
+ // (survives restarts and plugin reinstalls).
103
+ const SIDECAR_PATH = (() => {
104
+ try {
105
+ return join(dshHomePath('plugin-state'), 'caveman-state.json')
106
+ } catch (e) {
107
+ console.warn('[caveman] dshHomePath unavailable, sidecar persistence disabled:', e && e.message)
108
+ return null
109
+ }
110
+ })()
111
+ const sessionMode = new Map()
112
+ const loadSidecar = () => {
113
+ if (SIDECAR_PATH === null) return
114
+ try {
115
+ const data = JSON.parse(readFileSync(SIDECAR_PATH, 'utf8'))
116
+ const sessions = data && data.sessions
117
+ if (sessions && typeof sessions === 'object') {
118
+ for (const [id, mode] of Object.entries(sessions)) {
119
+ if (typeof mode === 'string' && VALID.includes(mode)) sessionMode.set(id, mode)
120
+ }
121
+ }
122
+ } catch (e) {
123
+ // missing or corrupt sidecar: start from an empty map (defaults apply)
124
+ }
125
+ }
126
+ const persistSidecar = () => {
127
+ if (SIDECAR_PATH === null) return
128
+ try {
129
+ mkdirSync(dirname(SIDECAR_PATH), { recursive: true })
130
+ const tmp = SIDECAR_PATH + '.tmp'
131
+ writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(sessionMode) }), 'utf8')
132
+ renameSync(tmp, SIDECAR_PATH)
133
+ } catch (e) {
134
+ console.warn('[caveman] sidecar persist failed:', e && e.message)
135
+ }
136
+ }
137
+ const readMode = (sessionId) => (sessionMode.has(sessionId) ? sessionMode.get(sessionId) : defaultMode)
138
+ const writeMode = (sessionId, mode) => {
139
+ sessionMode.set(sessionId, mode)
140
+ persistSidecar()
82
141
  }
142
+ loadSidecar()
83
143
 
84
144
  const commands = ctx.get('commands')
85
145
  if (commands !== undefined) {
@@ -92,7 +152,8 @@ export function apply(ctx) {
92
152
  if (!VALID.includes(mode)) {
93
153
  return { kind: 'error', text: 'Invalid mode: "' + invocation.rawInput.trim() + '". Valid: ' + VALID.join('|') }
94
154
  }
95
- invocation.agent.session.append('caveman/change', { mode })
155
+ const sid = invocation.agent && invocation.agent.session && invocation.agent.session.id
156
+ if (typeof sid === 'string' && sid.length > 0) writeMode(sid, mode)
96
157
  return { kind: 'success', text: mode === 'off' ? 'Caveman mode: off' : 'Caveman mode: ' + mode }
97
158
  },
98
159
  })
@@ -108,17 +169,48 @@ export function apply(ctx) {
108
169
  else kept.push(m)
109
170
  }
110
171
  if (mode === null) return next()
111
- payload.agent.session.append('caveman/change', { mode })
172
+ const sid = payload.agent && payload.agent.session && payload.agent.session.id
173
+ if (typeof sid === 'string' && sid.length > 0) writeMode(sid, mode)
112
174
  if (kept.length === 0) return { kind: 'reject' }
113
175
  return { kind: 'enter', messages: kept }
114
176
  })
115
177
 
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.
178
+ // ---- badge state read: GET /caveman/state?sessionId=... ----------------
179
+ // Fetch-mode badge client reads the per-session mode here (initial load
180
+ // plus a periodic re-sync). Exact route; the matcher strips the query.
120
181
  const CYCLE = ['off', 'lite', 'full', 'ultra']
121
182
  const webServer = ctx.get('webServer')
183
+ if (webServer !== undefined) {
184
+ try {
185
+ webServer.register({
186
+ kind: 'exact',
187
+ path: '/caveman/state',
188
+ handler: async (req, res) => {
189
+ if (req.method !== 'GET') {
190
+ res.writeHead(405)
191
+ res.end()
192
+ return
193
+ }
194
+ let sessionId = null
195
+ try {
196
+ sessionId = new URL(req.url || '/', 'http://x').searchParams.get('sessionId')
197
+ } catch (e) {
198
+ sessionId = null
199
+ }
200
+ const mode = typeof sessionId === 'string' && sessionId.length > 0 ? readMode(sessionId) : 'full'
201
+ res.writeHead(200, { 'content-type': 'application/json' })
202
+ res.end(JSON.stringify({ mode }))
203
+ },
204
+ })
205
+ } catch (e) {
206
+ console.error('[caveman] /caveman/state route registration failed:', e && e.message)
207
+ }
208
+ }
209
+
210
+ // ---- badge click toggle: POST /caveman/toggle { sessionId } -------------
211
+ // Cycles off -> lite -> full -> ultra -> off through the sidecar (the same
212
+ // mutation /caveman performs) and returns the new value so the badge can
213
+ // self-update from the response.
122
214
  if (webServer !== undefined) {
123
215
  try {
124
216
  webServer.register({
@@ -151,18 +243,9 @@ export function apply(ctx) {
151
243
  res.end(JSON.stringify({ ok: false, error: 'session not found' }))
152
244
  return
153
245
  }
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)
246
+ const at = CYCLE.indexOf(readMode(sessionId))
164
247
  const next = CYCLE[(at === -1 ? 0 : at) + 1] === undefined ? CYCLE[0] : CYCLE[at + 1]
165
- session.append('caveman/change', { mode: next })
248
+ writeMode(sessionId, next)
166
249
  res.writeHead(200, { 'content-type': 'application/json' })
167
250
  res.end(JSON.stringify({ ok: true, mode: next }))
168
251
  },
@@ -179,14 +262,9 @@ export function apply(ctx) {
179
262
  order: 50,
180
263
  text: (context) => {
181
264
  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
- }
265
+ if (!agent) return ''
266
+ const sid = agent.session && agent.session.id
267
+ const mode = typeof sid === 'string' && sid.length > 0 ? readMode(sid) : defaultMode
190
268
  if (mode === 'off') return ''
191
269
  const tag = mode === 'full' ? '[CAVEMAN]' : '[CAVEMAN:' + mode.toUpperCase() + ']'
192
270
  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 +272,5 @@ export function apply(ctx) {
194
272
  })
195
273
  }
196
274
 
197
- console.log('[caveman] host active (event projection + KNOWN registration)')
275
+ console.log('[caveman] host active (sidecar mode state + prompt section; writes no session events)')
198
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-caveman",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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.",
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"