dsh-caveman 0.1.1 → 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 +147 -64
  3. package/package.json +3 -6
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,31 +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
- projections.register({
65
- key: 'caveman',
66
- schema,
67
- stateVersion: 1,
68
- init: () => ({ mode: 'full' }),
69
- apply: (state, event) => {
70
- if (!event || event.type !== 'caveman/change') return state
71
- const mode = event.data && typeof event.data.mode === 'string' ? event.data.mode : ''
72
- if (!VALID.includes(mode) || state.mode === mode) return state
73
- return { mode }
74
- },
75
- view: (state) => ({ mode: state.mode }),
76
- })
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()
77
141
  }
142
+ loadSidecar()
78
143
 
79
144
  const commands = ctx.get('commands')
80
145
  if (commands !== undefined) {
@@ -87,7 +152,8 @@ export function apply(ctx) {
87
152
  if (!VALID.includes(mode)) {
88
153
  return { kind: 'error', text: 'Invalid mode: "' + invocation.rawInput.trim() + '". Valid: ' + VALID.join('|') }
89
154
  }
90
- 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)
91
157
  return { kind: 'success', text: mode === 'off' ? 'Caveman mode: off' : 'Caveman mode: ' + mode }
92
158
  },
93
159
  })
@@ -103,17 +169,48 @@ export function apply(ctx) {
103
169
  else kept.push(m)
104
170
  }
105
171
  if (mode === null) return next()
106
- 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)
107
174
  if (kept.length === 0) return { kind: 'reject' }
108
175
  return { kind: 'enter', messages: kept }
109
176
  })
110
177
 
111
- // ---- badge click toggle: POST /caveman/toggle { sessionId } -------------
112
- // The composer badge's onClick cycles the mode off -> lite -> full -> ultra
113
- // -> off through the same caveman/change event path as /caveman on|off, so
114
- // 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.
115
181
  const CYCLE = ['off', 'lite', 'full', 'ultra']
116
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.
117
214
  if (webServer !== undefined) {
118
215
  try {
119
216
  webServer.register({
@@ -146,18 +243,9 @@ export function apply(ctx) {
146
243
  res.end(JSON.stringify({ ok: false, error: 'session not found' }))
147
244
  return
148
245
  }
149
- let mode = 'full'
150
- if (projections !== undefined) {
151
- try {
152
- const value = projections.snapshot(session).values.caveman
153
- if (value && typeof value.mode === 'string') mode = value.mode
154
- } catch (e) {
155
- mode = 'full'
156
- }
157
- }
158
- const at = CYCLE.indexOf(mode)
246
+ const at = CYCLE.indexOf(readMode(sessionId))
159
247
  const next = CYCLE[(at === -1 ? 0 : at) + 1] === undefined ? CYCLE[0] : CYCLE[at + 1]
160
- session.append('caveman/change', { mode: next })
248
+ writeMode(sessionId, next)
161
249
  res.writeHead(200, { 'content-type': 'application/json' })
162
250
  res.end(JSON.stringify({ ok: true, mode: next }))
163
251
  },
@@ -174,14 +262,9 @@ export function apply(ctx) {
174
262
  order: 50,
175
263
  text: (context) => {
176
264
  const agent = context && context.agent
177
- if (!agent || projections === undefined) return ''
178
- let mode = 'full'
179
- try {
180
- const value = projections.snapshot(agent.session).values.caveman
181
- if (value && typeof value.mode === 'string') mode = value.mode
182
- } catch (err) {
183
- mode = 'full'
184
- }
265
+ if (!agent) return ''
266
+ const sid = agent.session && agent.session.id
267
+ const mode = typeof sid === 'string' && sid.length > 0 ? readMode(sid) : defaultMode
185
268
  if (mode === 'off') return ''
186
269
  const tag = mode === 'full' ? '[CAVEMAN]' : '[CAVEMAN:' + mode.toUpperCase() + ']'
187
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.'
@@ -189,5 +272,5 @@ export function apply(ctx) {
189
272
  })
190
273
  }
191
274
 
192
- console.log('[caveman] host active (event projection + KNOWN registration)')
193
- }
275
+ console.log('[caveman] host active (sidecar mode state + prompt section; writes no session events)')
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-caveman",
3
- "version": "0.1.1",
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"
@@ -35,4 +32,4 @@
35
32
  "inject": []
36
33
  }
37
34
  }
38
- }
35
+ }