dsh-engramory 0.1.0 → 0.2.0
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/README.md +41 -19
- package/cordis.patch.yml +16 -0
- package/index.js +138 -32
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# dsh-engramory
|
|
2
2
|
|
|
3
3
|
Curated, file-based long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —
|
|
4
|
-
plain markdown notes you can
|
|
5
|
-
use, and an index cap that is a **real refusal** rather than a request.
|
|
4
|
+
plain markdown notes you can open with anything, one store shared across every host you
|
|
5
|
+
use, and an index cap that is a **real refusal** rather than a request. (If the store
|
|
6
|
+
lives inside a project's git repo it must be git-ignored — memories carry machine-local
|
|
7
|
+
detail; see SKILL.md §1. A *dedicated private* repo for the store itself is fine.)
|
|
6
8
|
|
|
7
9
|
Part of [Engramory](https://github.com/tinqiao-oss/engramory).
|
|
8
10
|
|
|
@@ -16,7 +18,10 @@ because the index loads every session and everything past the cap silently stops
|
|
|
16
18
|
recalled. On most hosts that limit is rules plus a checker the agent has to remember to
|
|
17
19
|
run. dsh exposes `ctx.tools.guard()` — a synchronous, **monotonic** refusal: once a guard
|
|
18
20
|
returns a reason, no later listener can turn it back into an allow. So here the limit is
|
|
19
|
-
enforced, not requested
|
|
21
|
+
enforced, not requested — **once the plugin is actually installed and running**; a
|
|
22
|
+
profile install currently fails upstream (see "Known limits"), and until then the cap on
|
|
23
|
+
dsh is still rules plus the checker. Outside Claude Code, this is the only host where a
|
|
24
|
+
deterministic cap is even possible.
|
|
20
25
|
|
|
21
26
|
**The protocol arrives with the plugin.** `ctx.skills.register()` contributes the skill at
|
|
22
27
|
runtime, so it does not depend on landing files in one of the five skill roots dsh scans —
|
|
@@ -28,15 +33,22 @@ a path that is easy to get subtly wrong and fails silently when you do.
|
|
|
28
33
|
dsh plugin --profile <name> add dsh-engramory
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
That is the whole install. The package ships a `dsh.bundle` manifest pointing at its own
|
|
37
|
+
`cordis.patch.yml`, so the row is inserted into the profile's plugin tree for you — no
|
|
38
|
+
hand-editing. To change a default, patch the EXISTING row by id in your profile's own
|
|
39
|
+
patch layer — do **not** use a second `- insert:` (insert always appends, so you would
|
|
40
|
+
end up with two engramory rows and the original caps still enforced):
|
|
32
41
|
|
|
33
42
|
```yaml
|
|
34
43
|
- id: engramory
|
|
35
|
-
name: dsh-engramory
|
|
36
44
|
config:
|
|
37
|
-
indexName: MEMORY.md
|
|
45
|
+
indexName: MEMORY.md
|
|
46
|
+
maxLines: 200
|
|
47
|
+
maxBytes: 25600
|
|
38
48
|
```
|
|
39
49
|
|
|
50
|
+
A patch replaces the targeted row's whole `config`, so list every key you mean to keep.
|
|
51
|
+
|
|
40
52
|
For the store itself and the always-on block, use the installer in the Engramory repo
|
|
41
53
|
(`python tools/engramory_init.py dsh --install-skill`). This plugin enforces the cap and
|
|
42
54
|
supplies the protocol; it does not create the store.
|
|
@@ -45,7 +57,7 @@ supplies the protocol; it does not create the store.
|
|
|
45
57
|
|
|
46
58
|
| Field | Default | Meaning |
|
|
47
59
|
|---|---|---|
|
|
48
|
-
| `indexName` | `MEMORY.md` | Basename treated as the memory index. Nothing else is inspected. |
|
|
60
|
+
| `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked). An empty or non-string value falls back to the default. Nothing else is inspected. |
|
|
49
61
|
| `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
|
|
50
62
|
| `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
|
|
51
63
|
| `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
|
|
@@ -55,24 +67,34 @@ supplies the protocol; it does not create the store.
|
|
|
55
67
|
|
|
56
68
|
| Call | Decision |
|
|
57
69
|
|---|---|
|
|
58
|
-
| `write`
|
|
59
|
-
| `write`
|
|
60
|
-
|
|
|
61
|
-
|
|
|
70
|
+
| `write` (or `str_replace_editor` `create`) ending within caps | allow |
|
|
71
|
+
| `write`/`create` that GROWS the index past a cap | **deny**, naming the numbers and what to compact |
|
|
72
|
+
| `write`/`create` that SHRINKS or keeps an over-cap index | allow — incremental compaction (210 → 205 → 198) must stay possible |
|
|
73
|
+
| Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write |
|
|
74
|
+
| Unsimulable partial (e.g. `insert`) on an over-cap index | **deny**, telling the agent a shrinking whole-file write passes |
|
|
75
|
+
| Unsimulable partial on a healthy index | allow |
|
|
76
|
+
| `read`, `view`, or any unknown tool | allow — recall must never be blocked, even over cap |
|
|
62
77
|
| Any write to any other file | allow — not the guard's business |
|
|
63
|
-
| Missing/unreadable index
|
|
78
|
+
| Missing/unreadable current index on a whole-file write | treated as EMPTY (mirrors the Python guard) — a within-caps write passes, an over-cap first write is refused |
|
|
79
|
+
| Missing/unreadable index on a partial edit | allow — a guard must never block work over a path it cannot read |
|
|
80
|
+
| Malformed execution (no arguments, non-string path/content) | allow — not recognisably a write |
|
|
64
81
|
|
|
65
|
-
The
|
|
66
|
-
|
|
82
|
+
The deny rule mirrors `hooks/engramory_index_guard.py` exactly: refuse only a result
|
|
83
|
+
that is over a cap AND grew past the current file. The line count ignores a trailing
|
|
84
|
+
newline, so an index sitting exactly at the cap stays writable.
|
|
67
85
|
|
|
68
86
|
## Known limits
|
|
69
87
|
|
|
70
|
-
- **An
|
|
71
|
-
carry the resulting text
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
- **An unsimulable partial that crosses the cap from under it is not caught.** `insert`
|
|
89
|
+
and friends do not carry the resulting text. The breach is caught by the next
|
|
90
|
+
whole-file write, or by `engramory_check.py`. What is guaranteed: an already-over
|
|
91
|
+
index cannot be grown further, and a shrinking write always passes.
|
|
92
|
+
- **The tool roster follows dsh's documented tool-fs contract** (`write`/`edit` with
|
|
93
|
+
`file_path`, `str_replace_editor` with `path`) and is deliberately conservative:
|
|
94
|
+
unknown tools pass. Until the plugin runs inside a real profile this mapping is
|
|
95
|
+
documentation-verified only.
|
|
74
96
|
- **Not yet verified end to end inside dsh.** The guard's decision table is covered by
|
|
75
|
-
`node --test` (
|
|
97
|
+
`node --test` (20 cases, run in Engramory's CI), and the wiring for the AGENTS.md block
|
|
76
98
|
and skill discovery was dogfooded against a live `deepseek-v4-flash` session. But
|
|
77
99
|
installing a *third-party plugin* into a profile currently fails on dsh 0.1.0-rc.6:
|
|
78
100
|
`dsh plugin` shells out to `pnpm` (not bundled), and a direct `pnpm add` then dies on
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The dsh-engramory bundle patch: one row, inserted over whatever layers came before
|
|
2
|
+
# it. A patch replaces the targeted row's whole `config`, so a profile overriding this
|
|
3
|
+
# row must restate every key it means to keep.
|
|
4
|
+
#
|
|
5
|
+
# The numbers below are the protocol's own caps, not this port's opinion: 200 lines /
|
|
6
|
+
# 25 KB is what keeps MEMORY.md cheap enough to load at the start of every session, and
|
|
7
|
+
# it is the same limit hooks/engramory_index_guard.py enforces on Claude Code. They live
|
|
8
|
+
# here, beside the row, so a deployment can raise or lower them without touching code.
|
|
9
|
+
|
|
10
|
+
- insert:
|
|
11
|
+
- id: engramory
|
|
12
|
+
name: dsh-engramory
|
|
13
|
+
config:
|
|
14
|
+
indexName: MEMORY.md
|
|
15
|
+
maxLines: 200
|
|
16
|
+
maxBytes: 25600
|
package/index.js
CHANGED
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* sidesteps the five-root scan entirely, so the protocol is present because the
|
|
14
14
|
* plugin is loaded, not because a directory happened to be right.
|
|
15
15
|
*
|
|
16
|
+
* The decision table mirrors hooks/engramory_index_guard.py: deny only a write that
|
|
17
|
+
* ENDS over a cap AND grew past the current file — a shrinking rewrite always passes,
|
|
18
|
+
* so an over-cap index can be compacted incrementally (210 → 205 → 198). Only known
|
|
19
|
+
* mutating tools are gated; `read` and unknown tools are never refused (the first cut
|
|
20
|
+
* gated everything that named the index, which blocked recall of an over-cap index at
|
|
21
|
+
* exactly the moment compaction was needed).
|
|
22
|
+
*
|
|
16
23
|
* Zero dependencies, no build step: plain ESM, node: builtins only.
|
|
17
24
|
*/
|
|
18
25
|
import { readFileSync } from 'node:fs'
|
|
@@ -25,15 +32,25 @@ export const name = 'engramory'
|
|
|
25
32
|
export const inject = { required: ['tools'], optional: ['skills'] }
|
|
26
33
|
|
|
27
34
|
/** Mirrors hooks/engramory_index_guard.py — the caps are the protocol's, not this port's. */
|
|
35
|
+
const DEFAULT_INDEX_NAME = 'MEMORY.md'
|
|
28
36
|
const DEFAULT_MAX_LINES = 200
|
|
29
37
|
const DEFAULT_MAX_BYTES = 25600
|
|
30
38
|
|
|
31
|
-
/** Tools that replace a file wholesale, so
|
|
39
|
+
/** Tools that replace a file wholesale, so the post-write text is in the arguments. */
|
|
32
40
|
const WHOLE_FILE_WRITES = new Set(['write'])
|
|
33
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Tools that mutate a file in place. Only tools named here (plus
|
|
44
|
+
* `str_replace_editor`'s write commands, handled explicitly) count as partial
|
|
45
|
+
* writes. Unknown tools pass through: this guard is a discipline rail, not a
|
|
46
|
+
* security boundary (SECURITY.md), and a false refusal of `read` costs more than a
|
|
47
|
+
* miss — the next whole-file write is measured exactly regardless.
|
|
48
|
+
*/
|
|
49
|
+
const PARTIAL_WRITES = new Set(['edit', 'str_replace', 'insert'])
|
|
50
|
+
|
|
34
51
|
export function apply(ctx, config = {}) {
|
|
35
52
|
const settings = {
|
|
36
|
-
indexName: config.indexName
|
|
53
|
+
indexName: indexNameOf(config.indexName),
|
|
37
54
|
maxLines: positive(config.maxLines, DEFAULT_MAX_LINES),
|
|
38
55
|
maxBytes: positive(config.maxBytes, DEFAULT_MAX_BYTES),
|
|
39
56
|
}
|
|
@@ -62,52 +79,127 @@ export function apply(ctx, config = {}) {
|
|
|
62
79
|
*
|
|
63
80
|
* Guards are synchronous by contract, so this stays cheap: one basename comparison
|
|
64
81
|
* rejects the overwhelming majority of calls before anything is measured, and the
|
|
65
|
-
* only I/O is a single read of the index on a
|
|
82
|
+
* only I/O is a single read of the index on a gated write.
|
|
66
83
|
*/
|
|
67
84
|
function refuseOversizedIndex(exec, settings) {
|
|
68
85
|
const args = exec?.arguments
|
|
69
86
|
if (!args || typeof args !== 'object') return undefined
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
// dsh's file tools carry the target as `file_path`; `str_replace_editor` calls it
|
|
89
|
+
// `path`. Compare basenames case-INsensitively: on the case-insensitive
|
|
90
|
+
// filesystems most stores live on (Windows/macOS), `memory.md` IS the guarded
|
|
91
|
+
// index, and an exact compare let that spelling through. (Same trade-off as the
|
|
92
|
+
// Python guard: on a case-sensitive FS this can gate an unrelated lowercase twin —
|
|
93
|
+
// rename it, or point `indexName` elsewhere.)
|
|
94
|
+
const filePath = typeof args.file_path === 'string' ? args.file_path
|
|
95
|
+
: typeof args.path === 'string' ? args.path
|
|
96
|
+
: undefined
|
|
97
|
+
if (filePath === undefined) return undefined
|
|
98
|
+
if (basename(filePath).toLowerCase() !== settings.indexName.toLowerCase()) {
|
|
73
99
|
return undefined
|
|
74
100
|
}
|
|
75
101
|
|
|
76
|
-
|
|
102
|
+
const tool = typeof exec.name === 'string' ? exec.name : ''
|
|
103
|
+
|
|
104
|
+
if (WHOLE_FILE_WRITES.has(tool)) {
|
|
77
105
|
if (typeof args.content !== 'string') return undefined
|
|
78
|
-
return
|
|
106
|
+
return verdict(args.content, readIfPossible(filePath), filePath, settings)
|
|
107
|
+
}
|
|
108
|
+
if (tool === 'str_replace_editor' && args.command === 'create') {
|
|
109
|
+
if (typeof args.file_text !== 'string') return undefined
|
|
110
|
+
return verdict(args.file_text, readIfPossible(filePath), filePath, settings)
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
113
|
+
const partial = PARTIAL_WRITES.has(tool)
|
|
114
|
+
|| (tool === 'str_replace_editor'
|
|
115
|
+
&& (args.command === 'str_replace' || args.command === 'insert'))
|
|
116
|
+
if (!partial) return undefined // read / view / list / unknown: never refused
|
|
117
|
+
|
|
118
|
+
const currentBuf = readIfPossible(filePath)
|
|
119
|
+
if (currentBuf === undefined) return undefined
|
|
120
|
+
const current = currentBuf.toString('utf8')
|
|
121
|
+
const simulated = simulateEdit(current, args)
|
|
122
|
+
if (simulated !== undefined) {
|
|
123
|
+
return verdict(simulated, currentBuf, filePath, settings)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The edit's result genuinely cannot be reconstructed from its arguments. Honest,
|
|
127
|
+
// cheap rule: if the index is ALREADY over, refuse to mutate it blind — and name
|
|
128
|
+
// the open door, because "compact first" while refusing edits reads as a dead end.
|
|
88
129
|
const breach = measure(current, settings)
|
|
89
130
|
if (!breach) return undefined
|
|
90
131
|
return (
|
|
91
|
-
`${settings.indexName} is already over the Engramory cap (${breach})
|
|
92
|
-
`
|
|
93
|
-
`
|
|
94
|
-
`
|
|
95
|
-
`recalled.`
|
|
132
|
+
`${settings.indexName} is already over the Engramory cap (${breach}) and this ` +
|
|
133
|
+
`edit's result cannot be measured. Compact it with a whole-file write instead: ` +
|
|
134
|
+
`a rewrite that SHRINKS the index always passes, even while still over the cap. ` +
|
|
135
|
+
`Pointer-ify over-long lines, merge duplicates, archive cold notes — the index ` +
|
|
136
|
+
`is a table of contents, and anything past the cap silently stops being recalled.`
|
|
96
137
|
)
|
|
97
138
|
}
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Mirrors the Python guard's deny rule: refuse only when a dimension ends OVER its
|
|
142
|
+
* cap AND grew past the current file, so a shrinking/keeping write on an over-cap
|
|
143
|
+
* index always passes and incremental compaction stays possible. A missing or
|
|
144
|
+
* unreadable current file counts as empty — a first write past the cap is refused.
|
|
145
|
+
*/
|
|
146
|
+
function verdict(text, currentBuf, filePath, settings) {
|
|
147
|
+
const lines = countLines(text)
|
|
148
|
+
const bytes = Buffer.byteLength(text, 'utf8')
|
|
149
|
+
// Current byte size comes from the RAW buffer, mirroring how the Python guard
|
|
150
|
+
// sizes the on-disk file: decoding first inflated a non-UTF-8 index ~3x (every
|
|
151
|
+
// bad byte becomes a 3-byte U+FFFD), which made a genuinely growing write look
|
|
152
|
+
// like a shrink and pass. Newlines survive a lossy decode, so the line count may
|
|
153
|
+
// use the decoded text.
|
|
154
|
+
const curLines = currentBuf === undefined ? 0 : countLines(currentBuf.toString('utf8'))
|
|
155
|
+
const curBytes = currentBuf === undefined ? 0 : currentBuf.length
|
|
156
|
+
const over = []
|
|
157
|
+
if (lines > settings.maxLines && lines > curLines) {
|
|
158
|
+
over.push(`${lines} lines > ${settings.maxLines}`)
|
|
159
|
+
}
|
|
160
|
+
if (bytes > settings.maxBytes && bytes > curBytes) {
|
|
161
|
+
over.push(`${bytes} bytes > ${settings.maxBytes}`)
|
|
162
|
+
}
|
|
163
|
+
if (!over.length) return undefined
|
|
102
164
|
return (
|
|
103
165
|
`This write would put ${basename(filePath)} over the Engramory index cap ` +
|
|
104
|
-
`(${
|
|
105
|
-
`so anything past the cap silently stops being recalled. Compact before
|
|
106
|
-
`move detail into the linked note files, merge duplicates, archive cold
|
|
107
|
-
`and keep every line to "one short hook + link"
|
|
166
|
+
`(${over.join(', ')}). The index is loaded every session and the host only reads ` +
|
|
167
|
+
`so far, so anything past the cap silently stops being recalled. Compact before ` +
|
|
168
|
+
`writing: move detail into the linked note files, merge duplicates, archive cold ` +
|
|
169
|
+
`notes, and keep every line to "one short hook + link". A write that SHRINKS the ` +
|
|
170
|
+
`index always passes, so you can compact step by step.`
|
|
108
171
|
)
|
|
109
172
|
}
|
|
110
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Reconstruct a partial edit's post-write text when its arguments carry enough to
|
|
176
|
+
* do it (`old_str`/`new_str`, or the Claude-style `old_string`/`new_string`).
|
|
177
|
+
* Mirrors the Python guard's Edit simulation: replace the unique occurrence, or all
|
|
178
|
+
* of them under a replace-all flag; an absent or ambiguous old-string means the
|
|
179
|
+
* real tool errors and changes nothing, so the current text is the honest
|
|
180
|
+
* prediction. Returns undefined when the shape isn't recognised.
|
|
181
|
+
*/
|
|
182
|
+
function simulateEdit(current, args) {
|
|
183
|
+
const oldStr = firstString(args.old_str, args.old_string)
|
|
184
|
+
if (oldStr === undefined || oldStr === '') return undefined
|
|
185
|
+
const newStr = firstString(args.new_str, args.new_string) ?? ''
|
|
186
|
+
if (args.replace_all === true || args.replaceAll === true) {
|
|
187
|
+
return current.split(oldStr).join(newStr)
|
|
188
|
+
}
|
|
189
|
+
const first = current.indexOf(oldStr)
|
|
190
|
+
if (first === -1) return current // the real tool errors: nothing changes
|
|
191
|
+
const second = current.indexOf(oldStr, first + oldStr.length)
|
|
192
|
+
if (second !== -1) return current // ambiguous: the real tool errors, nothing changes
|
|
193
|
+
return current.slice(0, first) + newStr + current.slice(first + oldStr.length)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function firstString(...values) {
|
|
197
|
+
for (const v of values) {
|
|
198
|
+
if (typeof v === 'string') return v
|
|
199
|
+
}
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
111
203
|
/** Returns a human-readable breach description, or `null` when the text is within caps. */
|
|
112
204
|
function measure(text, { maxLines, maxBytes }) {
|
|
113
205
|
const lines = countLines(text)
|
|
@@ -126,18 +218,31 @@ function countLines(text) {
|
|
|
126
218
|
|
|
127
219
|
/**
|
|
128
220
|
* A missing or unreadable index is not a breach: the guard's job is to stop an
|
|
129
|
-
* oversized index, never to block work because a path could not be read.
|
|
221
|
+
* oversized index, never to block work because a path could not be read. Returns the
|
|
222
|
+
* RAW buffer — byte comparisons must use on-disk bytes (see verdict), decode at the
|
|
223
|
+
* call sites that need text.
|
|
130
224
|
*/
|
|
131
225
|
function readIfPossible(filePath) {
|
|
132
226
|
try {
|
|
133
|
-
return readFileSync(filePath
|
|
227
|
+
return readFileSync(filePath)
|
|
134
228
|
} catch {
|
|
135
229
|
return undefined
|
|
136
230
|
}
|
|
137
231
|
}
|
|
138
232
|
|
|
139
233
|
function positive(value, fallback) {
|
|
140
|
-
|
|
234
|
+
if (!Number.isFinite(value)) return fallback
|
|
235
|
+
const floored = Math.floor(value)
|
|
236
|
+
return floored > 0 ? floored : fallback
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* An empty or non-string indexName must fall back, not silently disable the cap:
|
|
241
|
+
* the numeric caps already recover from nonsense values, and this field is the one
|
|
242
|
+
* that decides whether the guard fires at all.
|
|
243
|
+
*/
|
|
244
|
+
function indexNameOf(value) {
|
|
245
|
+
return typeof value === 'string' && value.trim() ? value.trim() : DEFAULT_INDEX_NAME
|
|
141
246
|
}
|
|
142
247
|
|
|
143
248
|
/**
|
|
@@ -194,9 +299,10 @@ function builtinSkillBody() {
|
|
|
194
299
|
'',
|
|
195
300
|
'## The cap is enforced here',
|
|
196
301
|
'',
|
|
197
|
-
'This host denies a write that would
|
|
198
|
-
'a real refusal, not a warning
|
|
199
|
-
'
|
|
302
|
+
'This host denies a write that would GROW the index past 200 lines / 25 KB. That is',
|
|
303
|
+
'a real refusal, not a warning — but a write that shrinks the index always passes,',
|
|
304
|
+
'so compact first, step by step if needed: the index is loaded every session, and',
|
|
305
|
+
'anything past the cap silently stops being recalled.',
|
|
200
306
|
'',
|
|
201
307
|
'Never write credentials, keys, tokens, or cookies into memory — record only where',
|
|
202
308
|
'the secret lives.',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engramory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one plain-file store shared across hosts (git-ignored when it lives inside a project repo), and a deterministic index cap enforced by ctx.tools.guard()",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -14,12 +14,18 @@
|
|
|
14
14
|
],
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "index.js",
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
17
22
|
"exports": {
|
|
18
23
|
".": "./index.js",
|
|
19
24
|
"./package.json": "./package.json"
|
|
20
25
|
},
|
|
21
26
|
"files": [
|
|
22
27
|
"index.js",
|
|
28
|
+
"cordis.patch.yml",
|
|
23
29
|
"README.md",
|
|
24
30
|
"LICENSE"
|
|
25
31
|
],
|