dsh-global-todo 0.12.10 → 0.12.12
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/CHANGELOG.md +24 -0
- package/docs/DISTRIBUTION.md +2 -2
- package/lib/index.js +1 -1
- package/lib/routes.js +53 -5
- package/lib/settings.js +23 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## [0.12.12] — 2026-10-21
|
|
2
|
+
|
|
3
|
+
Three more silent coercions, found by reading the settings path instead of the tests.
|
|
4
|
+
|
|
5
|
+
- **A Windows workspace exemption only covered its exact path.** `isExemptWorkspace` tested
|
|
6
|
+
`${root}/`, so `C:\dev` matched itself but not `C:\dev\sub` — an exemption that looks
|
|
7
|
+
configured and silently asks conversations it should skip. Separators are now normalized and
|
|
8
|
+
the check is case-insensitive on Windows only.
|
|
9
|
+
- **`POST /settings` no longer coerces the wrong type.** The settings store is written for a
|
|
10
|
+
hand-edited file, where `autoCollect: "true"` becoming `false` and an unknown field being
|
|
11
|
+
ignored are reasonable; over HTTP they are typos that look successful. Booleans, arrays, the
|
|
12
|
+
token and the ingest sources are now type-checked, and an unknown key is `400` naming it.
|
|
13
|
+
- **A fractional minute is refused** (`{idleMinutes: 30.5}` used to fall back to the default with
|
|
14
|
+
a 200). Same rule as every other out-of-range value.
|
|
15
|
+
|
|
16
|
+
## [0.12.11] — 2026-10-21
|
|
17
|
+
|
|
18
|
+
One more silent fallback, found while re-reading the settings path.
|
|
19
|
+
|
|
20
|
+
- **`{idleMinutes: 30.5}` was accepted and then ignored.** `normalizeSettings` only keeps
|
|
21
|
+
integers for the minute fields and otherwise falls back to the default, so the request looked
|
|
22
|
+
like it worked while the value never changed. The settings route now rejects a fractional
|
|
23
|
+
minute with `400 invalid-body`, like every other out-of-range value.
|
|
24
|
+
|
|
1
25
|
## [0.12.10] — 2026-10-21
|
|
2
26
|
|
|
3
27
|
Two follow-ups from #3, plus the notification channel this repository actually has.
|
package/docs/DISTRIBUTION.md
CHANGED
|
@@ -15,10 +15,10 @@ of trusting this table.
|
|
|
15
15
|
|
|
16
16
|
| Registry | Form | What it gives you | Verified state |
|
|
17
17
|
|---|---|---|---|
|
|
18
|
-
| **npm** | the package | the install path | `dsh-global-todo@0.
|
|
18
|
+
| **npm** | the package | the install path | `dsh-global-todo@0.12.10` |
|
|
19
19
|
| **1024 Store** ([deepseek1024.com](https://deepseek1024.com)) · [repo](https://github.com/imsai-sh/awesome-deepseek-harness-plugins) | catalog entry, merged [#654](https://github.com/imsai-sh/awesome-deepseek-harness-plugins/pull/654) | searchable, has a public API | 🟡 entry live, but its install line still resolves to `github:LUgABeEEE/dsh-global-todo` — their npm enrichment has not caught up. Install works either way. |
|
|
20
20
|
| **dsh.pub** · [page](https://dsh.pub/en/plugins/dsh-global-todo/) | published page + `dshpub` CLI | `npx dshpub add LUgABeEEE/dsh-global-todo` | ✅ page live, pinned to a commit |
|
|
21
|
-
| **DSH-Plugins-Marketplace** ([bradeGithub](https://github.com/bradeGithub/DSH-Plugins-Marketplace)) | `registry.json`, collected from the `dsh-plugin` topic | one-click install inside DSH | ✅ `v0.
|
|
21
|
+
| **DSH-Plugins-Marketplace** ([bradeGithub](https://github.com/bradeGithub/DSH-Plugins-Marketplace)) | `registry.json`, collected from the `dsh-plugin` topic | one-click install inside DSH | ✅ `v0.12.x`, risk tier `safe` |
|
|
22
22
|
| **awesome-dsh-plugin** ([19.7k★](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)) | one YAML per plugin | the largest directory | ✅ merged [#7498](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/7498) |
|
|
23
23
|
| **walkinglabs/awesome-deepseek-harness-plugins** | README entry (source-verified list) | review-backed listing | ✅ merged [#114](https://github.com/walkinglabs/awesome-deepseek-harness-plugins/pull/114) |
|
|
24
24
|
|
package/lib/index.js
CHANGED
|
@@ -37,7 +37,7 @@ import { createStore, resolveStorePath } from './store.js'
|
|
|
37
37
|
import { createTools } from './tools.js'
|
|
38
38
|
|
|
39
39
|
/** Must equal `package.json`'s version; `npm run check` enforces that. */
|
|
40
|
-
export const VERSION = '0.12.
|
|
40
|
+
export const VERSION = '0.12.12'
|
|
41
41
|
|
|
42
42
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
43
43
|
export const name = 'dsh-global-todo'
|
package/lib/routes.js
CHANGED
|
@@ -28,6 +28,7 @@ import path from 'node:path'
|
|
|
28
28
|
import { TodoError, asTodoError } from './errors.js'
|
|
29
29
|
import { MAX_INGEST_ITEMS } from './ingest.js'
|
|
30
30
|
import { fromMarkdown, toMarkdown } from './markdown.js'
|
|
31
|
+
import { INGEST_SOURCES } from './settings.js'
|
|
31
32
|
import { weeklyReport } from './report.js'
|
|
32
33
|
|
|
33
34
|
/** Route prefix. The client appends this to the document base. */
|
|
@@ -535,6 +536,7 @@ export function createRoutes(deps) {
|
|
|
535
536
|
// The settings store clamps silently, which is right for a hand-edited file and wrong
|
|
536
537
|
// for an API: `{idleMinutes: -5}` answering 200-but-5 hides the rejected intent. A
|
|
537
538
|
// provided value outside its range is reported instead.
|
|
539
|
+
assertSettingsShape(body ?? {})
|
|
538
540
|
assertSettingsRange(body ?? {})
|
|
539
541
|
const next = await deps.settings.update(body ?? {})
|
|
540
542
|
return {
|
|
@@ -735,6 +737,47 @@ export function createRoutes(deps) {
|
|
|
735
737
|
]
|
|
736
738
|
}
|
|
737
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Types the settings route insists on, because the store coerces instead of rejecting.
|
|
742
|
+
*
|
|
743
|
+
* \`normalizeSettings\` is written for a hand-edited file: \`autoCollect: "true"\` becomes
|
|
744
|
+
* false, an unknown source is dropped, a typo silently changes nothing. That is the right
|
|
745
|
+
* behaviour when reading a file and the wrong behaviour for a request.
|
|
746
|
+
*
|
|
747
|
+
* @param {Record<string, unknown>} body
|
|
748
|
+
*/
|
|
749
|
+
function assertSettingsShape(body) {
|
|
750
|
+
const known = ['autoCollect', 'idleMinutes', 'cooldownMinutes', 'exemptWorkspaces', 'ingestEnabled', 'ingestSources', 'ingestConfirmBelow', 'ingestToken', 'remindersEnabled', 'reminderLeadMinutes']
|
|
751
|
+
for (const key of Object.keys(body)) {
|
|
752
|
+
if (!known.includes(key)) {
|
|
753
|
+
throw new TodoError('invalid-body', '未知的设置项 ' + key + ';可写的是 ' + known.join(' / '))
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const field of ['autoCollect', 'ingestEnabled', 'remindersEnabled']) {
|
|
757
|
+
const value = body[field]
|
|
758
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
759
|
+
throw new TodoError('invalid-body', field + ' 必须是 true / false,收到 ' + JSON.stringify(value))
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (body.ingestToken !== undefined && typeof body.ingestToken !== 'string') {
|
|
763
|
+
throw new TodoError('invalid-body', 'ingestToken 必须是字符串')
|
|
764
|
+
}
|
|
765
|
+
if (body.exemptWorkspaces !== undefined) {
|
|
766
|
+
if (!Array.isArray(body.exemptWorkspaces) || body.exemptWorkspaces.some((entry) => typeof entry !== 'string')) {
|
|
767
|
+
throw new TodoError('invalid-body', 'exemptWorkspaces 必须是字符串数组')
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (body.ingestSources !== undefined) {
|
|
771
|
+
if (!Array.isArray(body.ingestSources)) {
|
|
772
|
+
throw new TodoError('invalid-body', 'ingestSources 必须是数组')
|
|
773
|
+
}
|
|
774
|
+
const bad = body.ingestSources.filter((entry) => typeof entry !== 'string' || !INGEST_SOURCES.includes(entry))
|
|
775
|
+
if (bad.length > 0) {
|
|
776
|
+
throw new TodoError('invalid-body', 'ingestSources 只能是 ' + INGEST_SOURCES.join(' / ') + ',收到 ' + bad.join(', '))
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
738
781
|
/**
|
|
739
782
|
* Ranges the settings route refuses, mirroring the clamps in `normalizeSettings`.
|
|
740
783
|
*
|
|
@@ -742,17 +785,22 @@ export function createRoutes(deps) {
|
|
|
742
785
|
*/
|
|
743
786
|
function assertSettingsRange(body) {
|
|
744
787
|
const ranges = {
|
|
745
|
-
idleMinutes: [5, 24 * 60],
|
|
746
|
-
cooldownMinutes: [5, 24 * 60],
|
|
747
|
-
ingestConfirmBelow: [0, 1],
|
|
748
|
-
reminderLeadMinutes: [0, 24 * 60],
|
|
788
|
+
idleMinutes: [5, 24 * 60, true],
|
|
789
|
+
cooldownMinutes: [5, 24 * 60, true],
|
|
790
|
+
ingestConfirmBelow: [0, 1, false],
|
|
791
|
+
reminderLeadMinutes: [0, 24 * 60, true],
|
|
749
792
|
}
|
|
750
|
-
for (const [field, [low, high]] of Object.entries(ranges)) {
|
|
793
|
+
for (const [field, [low, high, whole]] of Object.entries(ranges)) {
|
|
751
794
|
const value = body[field]
|
|
752
795
|
if (value === undefined || value === null) continue
|
|
753
796
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
754
797
|
throw new TodoError('invalid-body', `${field} 必须是数字(${low}–${high})`)
|
|
755
798
|
}
|
|
799
|
+
// normalizeSettings only accepts integers for the minute fields and otherwise falls back
|
|
800
|
+
// to the default — a silently ignored 30.5 is the same trap as a silent clamp.
|
|
801
|
+
if (whole === true && !Number.isInteger(value)) {
|
|
802
|
+
throw new TodoError('invalid-body', `${field} 必须是整数分钟,收到 ${value}`)
|
|
803
|
+
}
|
|
756
804
|
if (value < low || value > high) {
|
|
757
805
|
throw new TodoError('invalid-body', `${field} 必须在 ${low}–${high} 之间,收到 ${value}`)
|
|
758
806
|
}
|
package/lib/settings.js
CHANGED
|
@@ -145,12 +145,31 @@ export function normalizeSettings(raw) {
|
|
|
145
145
|
* @param {string[]} exemptWorkspaces
|
|
146
146
|
* @returns {boolean}
|
|
147
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* One separator for comparisons, whatever the platform wrote.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} value
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function normalizeSeparators(value) {
|
|
155
|
+
return value.replace(/\\/g, '/')
|
|
156
|
+
}
|
|
148
157
|
export function isExemptWorkspace(sessionDir, exemptWorkspaces) {
|
|
149
158
|
if (typeof sessionDir !== 'string' || sessionDir === '') return false
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
159
|
+
// Compare on one separator: a Windows exemption of `C:\\dev` has to cover `C:\\dev\\sub`,
|
|
160
|
+
// and the old `${root}/` test made that false there while the exact match still worked —
|
|
161
|
+
// a half-working exemption is worse than none, because it looks configured.
|
|
162
|
+
const dir = normalizeSeparators(sessionDir).replace(/\/+$/, '')
|
|
163
|
+
const inWindows = process.platform === 'win32'
|
|
164
|
+
return (Array.isArray(exemptWorkspaces) ? exemptWorkspaces : []).some((root) => {
|
|
165
|
+
if (typeof root !== 'string' || root === '') return false
|
|
166
|
+
const base = normalizeSeparators(root).replace(/\/+$/, '')
|
|
167
|
+
const same = inWindows ? dir.toLowerCase() === base.toLowerCase() : dir === base
|
|
168
|
+
const inside = inWindows
|
|
169
|
+
? dir.toLowerCase().startsWith(`${base.toLowerCase()}/`)
|
|
170
|
+
: dir.startsWith(`${base}/`)
|
|
171
|
+
return same || inside
|
|
172
|
+
})
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
4
4
|
"icon": "./docs/icon.svg",
|
|
5
5
|
"description": "Cross-session todo list for DeepSeek Harness: one JSON file a person and an agent both write to, with step plans that ship as a numbered hand-off, session read-back, and a human confirmation before an agent's \"done\" counts.",
|
|
6
6
|
"type": "module",
|