@maci0/dsh-feynman 0.0.0-stage → 0.21.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.
package/lib/client.js ADDED
@@ -0,0 +1,350 @@
1
+ /**
2
+ * dsh-feynman browser half.
3
+ *
4
+ * One card on the Plugins page, bound to the settings form the settings domain
5
+ * serves for this package's `feynman` row. The card edits nothing in that
6
+ * namespace: the refs it shows are row config. It stages the two key
7
+ * literals locally and writes them through `remote.credentials`, so a literal
8
+ * never rides a settings response (the WebSearchCard pattern). The card learns
9
+ * only configured/writable booleans via `remote.credentials.describe`.
10
+ *
11
+ * Plain JavaScript on purpose: the client module system serves this file as a
12
+ * lazy-CJS factory on `window.__ModuleLoader__`; `react` is provided.
13
+ */
14
+
15
+ window.__ModuleLoader__.load({
16
+ id: '@maci0/dsh-feynman',
17
+
18
+ factory: (require) => {
19
+ var module = { exports: {} }
20
+ var exports = module.exports
21
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
22
+
23
+ const React = require('react')
24
+
25
+ /**
26
+ * This plugin's locale namespace and slot key. The settings form is
27
+ * separate: the settings domain keys one form per mounted row, so the card
28
+ * reads `feynman`, the row id in `cordis.patch.yml`.
29
+ */
30
+ const NAMESPACE = 'research-keys'
31
+
32
+ /** The row id whose config form the card edits. */
33
+ const SETTINGS_NAMESPACE = 'feynman'
34
+
35
+ const CSS = [
36
+ '.rk-body{border-top:0.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}',
37
+ '.rk-field{display:flex;flex-direction:column;gap:6px}',
38
+ '.rk-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
39
+ '.rk-row{display:flex;gap:8px}',
40
+ '.rk-input{flex:1;min-width:0;font:inherit;font-size:13px;padding:6px 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-4);border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
41
+ '.rk-btn{appearance:none;font:inherit;font-size:13px;padding:6px 14px;cursor:pointer;color:var(--dsw-alias-label-primary);background:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
42
+ '.rk-btn:disabled{cursor:default;opacity:.5}',
43
+ '.rk-badge{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',
44
+ '.rk-hint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',
45
+ '.rk-error{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}',
46
+ ].join('')
47
+
48
+ if (typeof document !== 'undefined') {
49
+ const style = document.createElement('style')
50
+ style.textContent = CSS
51
+ document.head.append(style)
52
+ }
53
+
54
+ const en = {
55
+ summary: 'Hugging Face and AlphaXiv keys, stored in the credentials file.',
56
+ set: 'set',
57
+ unset: 'not set',
58
+ unknown: 'unknown',
59
+ hfLabel: 'Hugging Face API key',
60
+ axLabel: 'AlphaXiv API key',
61
+ inputLabel: 'New value for {label}',
62
+ save: 'Save',
63
+ clear: 'Clear',
64
+ hint: 'Keys are stored in the managed credentials file, never in settings. Empty input clears nothing; use Clear.',
65
+ envNote: 'A key exported in the environment shadows the stored one.',
66
+ }
67
+
68
+ /** Slot id → settings ref field; labels resolve as `${id}Label`. */
69
+ const SLOTS = { hf: 'hfTokenEnv', ax: 'alphaxivTokenEnv' }
70
+
71
+ /**
72
+ * `/feynman` subcommand rows for the bare-invocation picker. Static copy
73
+ * of the host catalog (prompts.js WORKFLOWS + SESSION_COMMANDS); the
74
+ * client bundle cannot import the host module. Internal by design: the
75
+ * `/client` entry exports nothing beyond what cordis loading needs, and
76
+ * `client.test.js` asserts the rendered picker options instead.
77
+ */
78
+ const SUBCOMMANDS = [
79
+ ['deepresearch', '<topic>', 'Source-heavy investigation → research brief'],
80
+ ['lit', '<topic-or-lab>', 'Literature review: consensus, disagreements, open questions'],
81
+ ['review', '<arXiv-ID | URL | file>', 'Internal review with severity-graded feedback'],
82
+ ['review-loop', '<artifact> [rounds=3] | stop', 'Bounded review→fix→re-review loop'],
83
+ ['audit', '<arXiv-ID | repo-URL>', "Paper's claims vs its codebase"],
84
+ ['replicate', '<paper | claim>', 'Source-backed replication plan'],
85
+ ['recipe', '<training-task>', 'Ranked implementable ML training recipes'],
86
+ ['compare', '<topic | paper-IDs>', 'Agreement/disagreement matrix'],
87
+ ['draft', '<topic | --from-session>', 'Paper-style draft from findings'],
88
+ ['autoresearch', '<idea>', 'Hypothesis→experiment→analysis→decision loop'],
89
+ ['watch', '<topic>', 'Baseline survey + refresh plan'],
90
+ ['rank', '<topic> [flags]', 'Read-first paper ranking (PaperRank)'],
91
+ ['paper', '<id> [--fetch-full-text] [--json]', 'Legal full-text access candidates'],
92
+ ['preview', '[artifact-path]', 'Render an artifact via pandoc'],
93
+ ['log', '', 'Write a durable session log'],
94
+ ['jobs', '', 'Inspect background-job state'],
95
+ ['help', '', 'Show grouped research commands'],
96
+ ['feynman-model', '', 'How to change the model route'],
97
+ ['init', '', 'Bootstrap AGENTS.md and session-log folders'],
98
+ ['outputs', '', 'Browse research artifacts'],
99
+ ['btw', '<question>', 'Side question as non-waking context'],
100
+ ['thinking', '[level]', 'Note a thinking level'],
101
+ ['search', '<query>', 'Search prior session transcripts'],
102
+ ['web-results', '', 'List web sources fetched this session'],
103
+ ['keys', '', 'Show or store research API keys'],
104
+ ['doctor', '', 'Diagnose keys, seams, pandoc, card'],
105
+ ['status', '', 'Setup summary'],
106
+ ]
107
+
108
+ /** Configured-state word: ellipsis before an answer, `unknown` when the lookup failed. */
109
+ const word = (t, state) => state === undefined ? '…'
110
+ : state.unknown === true ? t('unknown') : t(state.configured ? 'set' : 'unset')
111
+
112
+ function refsOf(snapshot) {
113
+ const value = snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value : {}
114
+ return {
115
+ hfTokenEnv: typeof value.hfTokenEnv === 'string' ? value.hfTokenEnv : 'HF_TOKEN',
116
+ alphaxivTokenEnv: typeof value.alphaxivTokenEnv === 'string' ? value.alphaxivTokenEnv : 'ALPHAXIV_API_KEY',
117
+ }
118
+ }
119
+
120
+ /**
121
+ * One key row: status badge, password input, save/clear.
122
+ * The staged literal lives in component state and is cleared on save.
123
+ * @param remote - the browser remote context for credential writes.
124
+ */
125
+ function KeyRow(props) {
126
+ // NOTE: `ref` is reserved by React and never arrives as a prop;
127
+ // the credential reference rides `credRef` instead.
128
+ const { t, remote, label, credRef, state, onRefresh } = props
129
+ // `state` is undefined until the credentials domain answers for `credRef`;
130
+ // writes stay enabled meanwhile, and a refused write reports its error.
131
+ const writable = state === undefined || state.writable
132
+ const [staged, setStaged] = React.useState('')
133
+ const [error, setError] = React.useState(null)
134
+ const [busy, setBusy] = React.useState(false)
135
+
136
+ const run = async (fn) => {
137
+ setError(null)
138
+ setBusy(true)
139
+ try {
140
+ const response = await fn()
141
+ if (!response.ok) setError(response.error.message)
142
+ else setStaged('')
143
+ onRefresh()
144
+ } catch (cause) {
145
+ setError(cause instanceof Error ? cause.message : String(cause))
146
+ } finally {
147
+ setBusy(false)
148
+ }
149
+ }
150
+
151
+ return React.createElement(
152
+ 'div',
153
+ { className: 'rk-field' },
154
+ React.createElement('span', { className: 'rk-label' }, label),
155
+ React.createElement('span', { className: 'rk-badge', title: state?.failure }, `${credRef}: ${word(t, state)}`),
156
+ React.createElement(
157
+ 'div',
158
+ { className: 'rk-row' },
159
+ React.createElement('input', {
160
+ type: 'password',
161
+ className: 'rk-input',
162
+ value: staged,
163
+ autoComplete: 'off',
164
+ spellCheck: false,
165
+ disabled: busy || !writable,
166
+ // The label beside the field is a plain span, so the field names
167
+ // itself: without this a screen reader announces only "password".
168
+ 'aria-label': t('inputLabel', { label }),
169
+ onChange: (event) => { setStaged(event.target.value) },
170
+ }),
171
+ React.createElement(
172
+ 'button',
173
+ {
174
+ type: 'button',
175
+ className: 'rk-btn',
176
+ disabled: busy || !writable || staged === '',
177
+ onClick: () => { void run(() => remote.credentials.set(credRef, staged)) },
178
+ },
179
+ t('save'),
180
+ ),
181
+ state?.configured === true
182
+ ? React.createElement(
183
+ 'button',
184
+ {
185
+ type: 'button',
186
+ className: 'rk-btn',
187
+ disabled: busy || !writable,
188
+ onClick: () => { void run(() => remote.credentials.unset(credRef)) },
189
+ },
190
+ t('clear'),
191
+ )
192
+ : null,
193
+ ),
194
+ error === null ? null : React.createElement('div', { className: 'rk-error' }, error),
195
+ )
196
+ }
197
+
198
+ function createCard(scope, remote, t) {
199
+ const useKeys = () => React.useSyncExternalStore(
200
+ (listener) => scope.subscribe(listener),
201
+ () => scope.getSnapshot(),
202
+ )
203
+ /** Live card refreshers, so a key written elsewhere updates open cards. */
204
+ const refreshers = new Set()
205
+
206
+ const Card = function ResearchKeysCard(props) {
207
+ const snapshot = useKeys()
208
+ const [states, setStates] = React.useState({})
209
+ const refreshGeneration = React.useRef(0)
210
+ const ready = snapshot.status === 'ready'
211
+ const summary = props != null && props.view === 'summary'
212
+ const refs = refsOf(snapshot)
213
+
214
+ const refresh = async () => {
215
+ const started = ++refreshGeneration.current
216
+ // The reference is resolved at call time: a rename while a read is in
217
+ // flight must not make the next read answer for the old reference.
218
+ const live = refsOf(scope.getSnapshot())
219
+ const entries = await Promise.all(Object.entries(SLOTS).map(async ([id, refField]) => {
220
+ const ref = live[refField]
221
+ let failure
222
+ const response = await remote.credentials.describe([ref]).catch((cause) => {
223
+ failure = cause instanceof Error ? cause.message : String(cause)
224
+ return undefined
225
+ })
226
+ // Coerce at the wire boundary: only strict booleans reach state,
227
+ // so no response shape can ever render as a child.
228
+ const view = response !== null && typeof response === 'object' && response.ok === true
229
+ ? response.value?.[ref]
230
+ : undefined
231
+ const info = view !== null && typeof view === 'object' ? view : undefined
232
+ // A lookup with no usable answer says nothing about the key, so it
233
+ // reads unknown (reason as the tooltip), never "not set".
234
+ if (info === undefined) {
235
+ const refused = response !== null && typeof response === 'object' && response.ok === false
236
+ && typeof response.error?.message === 'string' ? response.error.message : undefined
237
+ const reason = failure ?? refused
238
+ return [id, { ref, unknown: true, writable: true, ...reason === undefined ? {} : { failure: reason } }]
239
+ }
240
+ // The answer carries the reference it describes. Two reads can
241
+ // settle out of order, and the reference can be renamed between
242
+ // them, so a state without that stamp is dropped at render.
243
+ return [id, { ref, configured: info?.configured === true, writable: info?.writable !== false }]
244
+ }))
245
+ if (started === refreshGeneration.current) setStates(Object.fromEntries(entries))
246
+ }
247
+
248
+ // Every hook runs before any early return: the form starts `loading`
249
+ // under the same mounted card, and React rejects a render that calls
250
+ // more hooks than the one before it. A rename re-runs the read, since
251
+ // no credential changed and no invalidation will arrive.
252
+ React.useEffect(() => {
253
+ if (!ready || summary) return undefined
254
+ refreshers.add(refresh)
255
+ void refresh()
256
+ return () => { refreshGeneration.current += 1; refreshers.delete(refresh) }
257
+ }, [ready, summary, refs.hfTokenEnv, refs.alphaxivTokenEnv])
258
+
259
+ if (!ready) return null
260
+ if (summary) return t('summary')
261
+
262
+ return React.createElement(
263
+ 'div',
264
+ { className: 'rk-page' },
265
+ React.createElement(
266
+ 'div',
267
+ { className: 'rk-body' },
268
+ Object.entries(SLOTS).map(([id, refField]) => {
269
+ // An answer for a reference the card no longer shows is no
270
+ // answer: reporting it would name one reference's state under
271
+ // another's label. It stays unknown until its own read lands.
272
+ const answered = states[id]
273
+ const state = answered !== undefined && answered.ref === refs[refField] ? answered : undefined
274
+ return React.createElement(KeyRow, {
275
+ key: `${id}:${refs[refField]}`,
276
+ t,
277
+ remote,
278
+ label: `${t(`${id}Label`)} (${refs[refField]})`,
279
+ credRef: refs[refField],
280
+ state,
281
+ onRefresh: () => { void refresh() },
282
+ })
283
+ }),
284
+ React.createElement('div', { className: 'rk-hint' }, `${t('hint')} ${t('envNote')}`),
285
+ ),
286
+ )
287
+ }
288
+
289
+ return { Card, refreshers }
290
+ }
291
+
292
+ /**
293
+ * Mount the card: locale, scope, slot registration, credential invalidation.
294
+ * Plus the `/feynman` bare-invocation picker: a subcommand popup whose
295
+ * pick submits the completed `/feynman <sub>` line back through the host,
296
+ * so the row offers subcommand completion the catalog row cannot.
297
+ * @param ctx - the browser plugin context.
298
+ */
299
+ function apply(ctx) {
300
+ const t = ctx.locale.bind(NAMESPACE)
301
+ // Single-locale registration: English is the documented fallback locale,
302
+ // and the per-key fallback resolves every other language. Registering a
303
+ // second namespace entry here would shadow later dictionaries.
304
+ ctx.effect(
305
+ () => ctx.locale.register(NAMESPACE, 'en', en),
306
+ 'dsh-feynman: locale dictionary',
307
+ )
308
+
309
+ const scope = ctx.configForms.get(SETTINGS_NAMESPACE)
310
+ const { Card, refreshers } = createCard(scope, ctx.remote, t)
311
+
312
+ ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
313
+ name: 'plugins.row.config',
314
+ key: '@maci0/dsh-feynman#feynman',
315
+ locale: NAMESPACE,
316
+ }, Card))
317
+
318
+ ctx.effect(
319
+ () => ctx.remote.$on('credentials/reference-updated', () => {
320
+ for (const refresh of refreshers) void refresh()
321
+ }),
322
+ 'dsh-feynman: credential invalidations',
323
+ )
324
+
325
+ ctx.effect(() => ctx.commandUi.decorate({
326
+ name: 'feynman',
327
+ available: () => true,
328
+ ui: {
329
+ kind: 'popupSelect',
330
+ options: () => Promise.resolve(SUBCOMMANDS.map(([id, hint, detail]) => ({
331
+ id,
332
+ label: hint ? `/feynman ${id} ${hint}` : `/feynman ${id}`,
333
+ ...(detail === undefined ? {} : { detail }),
334
+ }))),
335
+ onSelect: async (option, session) => {
336
+ const live = ctx.sessions.binding(session.sessionId)?.session
337
+ if (live === undefined) throw new Error('this session is not materialized yet')
338
+ const result = await live.command(`/feynman ${option.id} `)
339
+ if (!result.ok) throw new Error(`subcommand submit failed: ${result.error.code}: ${result.error.message}`)
340
+ if (!result.value.matched) throw new Error('the host offers no /feynman command')
341
+ },
342
+ },
343
+ }), 'dsh-feynman: /feynman subcommand picker')
344
+ }
345
+
346
+ exports.apply = apply
347
+ exports.inject = ['slots', 'configForms', 'locale', 'remote', 'remote.credentials', 'commandUi', 'sessions']
348
+ return module.exports
349
+ },
350
+ })
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Feynman",
4
+ "description": "Research slash commands for review, audit, and literature workflows."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "费曼",
4
+ "description": "研究用斜杠命令:审稿、审计与文献流程。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@maci0/dsh-feynman",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.21.3",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "type": "module",
9
+ "main": "index.js",
10
+ "description": "Feynman-style research workflows for DeepSeek Harness behind one /feynman command (deepresearch, review, audit, review-loop, \u2026)",
11
+ "license": "MIT",
12
+ "exports": {
13
+ ".": "./index.js",
14
+ "./client": {
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./cordis.patch.yml": "./cordis.patch.yml",
18
+ "./locale/*.json": "./locale/*.json",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ },
25
+ "client": {
26
+ "platform": "web",
27
+ "inject": [
28
+ "@deepseek-ai/dsh-client-connection",
29
+ "@deepseek-ai/dsh-client-locale",
30
+ "@deepseek-ai/dsh-client-ui-renderer",
31
+ "@deepseek-ai/dsh-client-ui-settings",
32
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
33
+ "@deepseek-ai/dsh-client-ui-commands",
34
+ "@deepseek-ai/dsh-api-session-controller"
35
+ ]
36
+ },
37
+ "compatibility": {
38
+ "dsh": ">=0.2.0-rc.2 <0.3.0"
39
+ }
40
+ },
41
+ "files": [
42
+ "icon.svg",
43
+ "locale/*.json",
44
+ "index.js",
45
+ "prompts.js",
46
+ "lib/client.js",
47
+ "cordis.patch.yml",
48
+ "README.md",
49
+ "LICENSE",
50
+ "NOTICE"
51
+ ],
52
+ "scripts": {
53
+ "test": "bun test",
54
+ "test:node": "node --test tests/*.test.*"
55
+ },
56
+ "engines": {
57
+ "node": "^22.19.0 || >=24.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@deepseek-ai/cordis": "4.0.5-alpha.1"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/maci0/dsh-feynman.git"
65
+ },
66
+ "icon": "./icon.svg",
67
+ "dependencies": {
68
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
69
+ "@deepseek-ai/schemastery": "3.18.5-alpha.1"
70
+ }
71
+ }