dsh-engramory 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/index.js +204 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Beijing Tinqiao Technology Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# dsh-engramory
|
|
2
|
+
|
|
3
|
+
Curated, file-based long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —
|
|
4
|
+
plain markdown notes you can read and `git log`, one store shared across every host you
|
|
5
|
+
use, and an index cap that is a **real refusal** rather than a request.
|
|
6
|
+
|
|
7
|
+
Part of [Engramory](https://github.com/tinqiao-oss/engramory).
|
|
8
|
+
|
|
9
|
+
## Why a plugin, when an `AGENTS.md` block already works
|
|
10
|
+
|
|
11
|
+
The block does carry the discipline, and for recall that is enough. Two things it cannot
|
|
12
|
+
do:
|
|
13
|
+
|
|
14
|
+
**The cap becomes deterministic.** Engramory keeps its index under 200 lines / 25 KB
|
|
15
|
+
because the index loads every session and everything past the cap silently stops being
|
|
16
|
+
recalled. On most hosts that limit is rules plus a checker the agent has to remember to
|
|
17
|
+
run. dsh exposes `ctx.tools.guard()` — a synchronous, **monotonic** refusal: once a guard
|
|
18
|
+
returns a reason, no later listener can turn it back into an allow. So here the limit is
|
|
19
|
+
enforced, not requested. Outside Claude Code, this is the only host where that is true.
|
|
20
|
+
|
|
21
|
+
**The protocol arrives with the plugin.** `ctx.skills.register()` contributes the skill at
|
|
22
|
+
runtime, so it does not depend on landing files in one of the five skill roots dsh scans —
|
|
23
|
+
a path that is easy to get subtly wrong and fails silently when you do.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dsh plugin --profile <name> add dsh-engramory
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then mount it in that profile's `cordis.patch.yml`:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
- id: engramory
|
|
35
|
+
name: dsh-engramory
|
|
36
|
+
config:
|
|
37
|
+
indexName: MEMORY.md # optional
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For the store itself and the always-on block, use the installer in the Engramory repo
|
|
41
|
+
(`python tools/engramory_init.py dsh --install-skill`). This plugin enforces the cap and
|
|
42
|
+
supplies the protocol; it does not create the store.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
| Field | Default | Meaning |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `indexName` | `MEMORY.md` | Basename treated as the memory index. Nothing else is inspected. |
|
|
49
|
+
| `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
|
|
50
|
+
| `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
|
|
51
|
+
| `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
|
|
52
|
+
| `skill` | built-in | Replace the skill body with your own markdown. |
|
|
53
|
+
|
|
54
|
+
## What the guard actually does
|
|
55
|
+
|
|
56
|
+
| Call | Decision |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `write` to the index, within caps | allow |
|
|
59
|
+
| `write` to the index, over either cap | **deny**, naming the numbers and what to compact |
|
|
60
|
+
| Partial edit of an index that is already over cap | **deny** — do not grow a breach |
|
|
61
|
+
| Partial edit of a healthy index | allow |
|
|
62
|
+
| Any write to any other file | allow — not the guard's business |
|
|
63
|
+
| Missing/unreadable index, malformed arguments | allow — a guard must never block work by accident |
|
|
64
|
+
|
|
65
|
+
The line count ignores a trailing newline, matching `hooks/engramory_index_guard.py`, so
|
|
66
|
+
an index sitting exactly at the cap stays writable.
|
|
67
|
+
|
|
68
|
+
## Known limits
|
|
69
|
+
|
|
70
|
+
- **An edit that crosses the cap from under it is not caught.** A partial write does not
|
|
71
|
+
carry the resulting text, and a synchronous guard has no business reconstructing it.
|
|
72
|
+
The breach is caught by the next whole-file write, or by `engramory_check.py`. What is
|
|
73
|
+
guaranteed here is that an already-over index cannot be grown further.
|
|
74
|
+
- **Not yet verified end to end inside dsh.** The guard's decision table is covered by
|
|
75
|
+
`node --test` (13 cases, run in Engramory's CI), and the wiring for the AGENTS.md block
|
|
76
|
+
and skill discovery was dogfooded against a live `deepseek-v4-flash` session. But
|
|
77
|
+
installing a *third-party plugin* into a profile currently fails on dsh 0.1.0-rc.6:
|
|
78
|
+
`dsh plugin` shells out to `pnpm` (not bundled), and a direct `pnpm add` then dies on
|
|
79
|
+
`ERR_PNPM_FETCH_404` for `@deepseek-ai/dsh-type-meta`, a package the published tree
|
|
80
|
+
depends on but which is not on the registry. That is a preview-packaging problem
|
|
81
|
+
upstream, not a plugin defect; this note goes away once a profile install resolves.
|
|
82
|
+
- dsh is a developer preview and its plugin API can change. This plugin deliberately
|
|
83
|
+
touches only `ctx.tools.guard()` and `ctx.skills.register()` so it stays cheap to fix.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT — see the [Engramory repository](https://github.com/tinqiao-oss/engramory).
|
package/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-engramory — the Engramory memory discipline as a DeepSeek Harness plugin.
|
|
3
|
+
*
|
|
4
|
+
* Two things the always-on AGENTS.md block cannot do on its own:
|
|
5
|
+
*
|
|
6
|
+
* 1. A DETERMINISTIC index cap. dsh's `ctx.tools.guard()` is a synchronous,
|
|
7
|
+
* monotonic refusal — once a guard returns a reason, no later waterfall
|
|
8
|
+
* listener can turn it back into an allow. That makes it a stronger seam than
|
|
9
|
+
* most hosts expose, and it is why the 200-line / 25 KB limit can be enforced
|
|
10
|
+
* here rather than merely asked for. Everywhere except Claude Code, Engramory's
|
|
11
|
+
* cap degrades to "rules plus a checker the agent has to remember to run".
|
|
12
|
+
* 2. Skill delivery that does not depend on install paths. Registering at runtime
|
|
13
|
+
* sidesteps the five-root scan entirely, so the protocol is present because the
|
|
14
|
+
* plugin is loaded, not because a directory happened to be right.
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies, no build step: plain ESM, node: builtins only.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { basename } from 'node:path'
|
|
20
|
+
|
|
21
|
+
export const name = 'engramory'
|
|
22
|
+
|
|
23
|
+
// `tools` carries the cap and is required. `skills` is optional so the plugin still
|
|
24
|
+
// loads (and still caps) on a profile that mounts no skill registry.
|
|
25
|
+
export const inject = { required: ['tools'], optional: ['skills'] }
|
|
26
|
+
|
|
27
|
+
/** Mirrors hooks/engramory_index_guard.py — the caps are the protocol's, not this port's. */
|
|
28
|
+
const DEFAULT_MAX_LINES = 200
|
|
29
|
+
const DEFAULT_MAX_BYTES = 25600
|
|
30
|
+
|
|
31
|
+
/** Tools that replace a file wholesale, so `arguments.content` IS the post-write text. */
|
|
32
|
+
const WHOLE_FILE_WRITES = new Set(['write'])
|
|
33
|
+
|
|
34
|
+
export function apply(ctx, config = {}) {
|
|
35
|
+
const settings = {
|
|
36
|
+
indexName: config.indexName ?? 'MEMORY.md',
|
|
37
|
+
maxLines: positive(config.maxLines, DEFAULT_MAX_LINES),
|
|
38
|
+
maxBytes: positive(config.maxBytes, DEFAULT_MAX_BYTES),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ctx.tools.guard((exec) => refuseOversizedIndex(exec, settings))
|
|
42
|
+
|
|
43
|
+
if (ctx.skills && config.registerSkill !== false) {
|
|
44
|
+
const skill = config.skill ?? builtinSkillBody()
|
|
45
|
+
ctx.skills.register({
|
|
46
|
+
name: 'engramory',
|
|
47
|
+
description:
|
|
48
|
+
'Curated file-based long-term memory: recall through MEMORY.md at the start of ' +
|
|
49
|
+
'a task, save durable user/feedback/project/reference facts, and sync before ' +
|
|
50
|
+
'compacting or opening a fresh thread.',
|
|
51
|
+
whenToUse:
|
|
52
|
+
'Starting or resuming work, learning something durable worth a future session, ' +
|
|
53
|
+
'or approaching a compact/clear/new-thread boundary.',
|
|
54
|
+
source: 'runtime',
|
|
55
|
+
content: skill,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The cap itself. Returning a string denies the call; `undefined` lets it through.
|
|
62
|
+
*
|
|
63
|
+
* Guards are synchronous by contract, so this stays cheap: one basename comparison
|
|
64
|
+
* 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 partial write.
|
|
66
|
+
*/
|
|
67
|
+
function refuseOversizedIndex(exec, settings) {
|
|
68
|
+
const args = exec?.arguments
|
|
69
|
+
if (!args || typeof args !== 'object') return undefined
|
|
70
|
+
|
|
71
|
+
const filePath = args.file_path
|
|
72
|
+
if (typeof filePath !== 'string' || basename(filePath) !== settings.indexName) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (WHOLE_FILE_WRITES.has(exec.name)) {
|
|
77
|
+
if (typeof args.content !== 'string') return undefined
|
|
78
|
+
return verdictFor(args.content, filePath, settings)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A partial write (an edit / replace) does not carry the resulting text, and a
|
|
82
|
+
// guard must not do expensive work to reconstruct it. The honest, cheap rule: if
|
|
83
|
+
// the index is ALREADY over, refuse to grow it further. An edit that crosses the
|
|
84
|
+
// line from under it still gets through here — engramory_check.py is the backstop,
|
|
85
|
+
// and the next whole-file write is caught exactly.
|
|
86
|
+
const current = readIfPossible(filePath)
|
|
87
|
+
if (current === undefined) return undefined
|
|
88
|
+
const breach = measure(current, settings)
|
|
89
|
+
if (!breach) return undefined
|
|
90
|
+
return (
|
|
91
|
+
`${settings.indexName} is already over the Engramory cap (${breach}). ` +
|
|
92
|
+
`Editing it further would push it deeper past the limit. Compact it first: ` +
|
|
93
|
+
`pointer-ify over-long lines, merge duplicates, and archive cold notes — the ` +
|
|
94
|
+
`index is a table of contents, and anything past the cap silently stops being ` +
|
|
95
|
+
`recalled.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function verdictFor(text, filePath, settings) {
|
|
100
|
+
const breach = measure(text, settings)
|
|
101
|
+
if (!breach) return undefined
|
|
102
|
+
return (
|
|
103
|
+
`This write would put ${basename(filePath)} over the Engramory index cap ` +
|
|
104
|
+
`(${breach}). The index is loaded every session and the host only reads so far, ` +
|
|
105
|
+
`so anything past the cap silently stops being recalled. Compact before writing: ` +
|
|
106
|
+
`move detail into the linked note files, merge duplicates, archive cold notes, ` +
|
|
107
|
+
`and keep every line to "one short hook + link".`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Returns a human-readable breach description, or `null` when the text is within caps. */
|
|
112
|
+
function measure(text, { maxLines, maxBytes }) {
|
|
113
|
+
const lines = countLines(text)
|
|
114
|
+
const bytes = Buffer.byteLength(text, 'utf8')
|
|
115
|
+
const over = []
|
|
116
|
+
if (lines > maxLines) over.push(`${lines} lines > ${maxLines}`)
|
|
117
|
+
if (bytes > maxBytes) over.push(`${bytes} bytes > ${maxBytes}`)
|
|
118
|
+
return over.length ? over.join(', ') : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Trailing-newline-insensitive, matching how the Python guard counts. */
|
|
122
|
+
function countLines(text) {
|
|
123
|
+
if (!text) return 0
|
|
124
|
+
return text.split('\n').length - (text.endsWith('\n') ? 1 : 0)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 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.
|
|
130
|
+
*/
|
|
131
|
+
function readIfPossible(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return readFileSync(filePath, 'utf8')
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function positive(value, fallback) {
|
|
140
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The always-on block states the discipline; this is the on-demand detail the model
|
|
145
|
+
* pulls when it is actually about to recall, write, or sync. Kept deliberately short —
|
|
146
|
+
* the full protocol is SKILL.md in the Engramory repo, and a skill body that has to be
|
|
147
|
+
* read before it is useful is a skill that will not be read.
|
|
148
|
+
*/
|
|
149
|
+
function builtinSkillBody() {
|
|
150
|
+
return [
|
|
151
|
+
'# Engramory — curated file-based memory',
|
|
152
|
+
'',
|
|
153
|
+
'One canonical store: `MEMORY.md` is an index of pointers, one line per memory;',
|
|
154
|
+
'each fact lives in its own small markdown file beside it. Never put content in',
|
|
155
|
+
'the index, and never keep a second parallel store for handoffs.',
|
|
156
|
+
'',
|
|
157
|
+
'## Recall',
|
|
158
|
+
'',
|
|
159
|
+
'At the start of a task, read the index and open only the notes whose hooks look',
|
|
160
|
+
'relevant. Treat what you recall as background that may be stale: re-verify any',
|
|
161
|
+
'file, flag, or version against the repo before acting on it.',
|
|
162
|
+
'',
|
|
163
|
+
'## Write',
|
|
164
|
+
'',
|
|
165
|
+
'Before writing, confirm the fact is not already in the repo, git history, or the',
|
|
166
|
+
'instruction files, and that it is not a secret value. Search the index and update',
|
|
167
|
+
'an existing note rather than adding a near-duplicate. A new note is one atomic',
|
|
168
|
+
'fact with frontmatter:',
|
|
169
|
+
'',
|
|
170
|
+
'```markdown',
|
|
171
|
+
'---',
|
|
172
|
+
'name: <kebab-case-slug>',
|
|
173
|
+
'description: <one sharp line — this is what future-you reads to decide to open it>',
|
|
174
|
+
'type: user | feedback | project | reference',
|
|
175
|
+
'scope: global | repo # optional: does this still hold in another repo?',
|
|
176
|
+
'created: YYYY-MM-DD',
|
|
177
|
+
'updated: YYYY-MM-DD',
|
|
178
|
+
'---',
|
|
179
|
+
'```',
|
|
180
|
+
'',
|
|
181
|
+
'`feedback` and `project` notes MUST carry a `**Why:**` line and a',
|
|
182
|
+
'`**How to apply:**` line. Add exactly one pointer line to the index, and delete',
|
|
183
|
+
'memories that turn out to be wrong.',
|
|
184
|
+
'',
|
|
185
|
+
'Store settled facts, never current state: "2.0 shipped on 2026-01-15" is durable;',
|
|
186
|
+
'"the current version is X", the tip commit, or a passing test count will rot —',
|
|
187
|
+
'record where to read those instead.',
|
|
188
|
+
'',
|
|
189
|
+
'## Sync',
|
|
190
|
+
'',
|
|
191
|
+
'Before a deliberate compact, clear, or new thread: scan the task, dedup and update,',
|
|
192
|
+
'refresh project state, promote only reusable feedback, retire completed transient',
|
|
193
|
+
'state, then report what was added, updated, archived, and skipped.',
|
|
194
|
+
'',
|
|
195
|
+
'## The cap is enforced here',
|
|
196
|
+
'',
|
|
197
|
+
'This host denies a write that would push the index past 200 lines / 25 KB. That is',
|
|
198
|
+
'a real refusal, not a warning: compact first — the index is loaded every session,',
|
|
199
|
+
'and anything past the cap silently stops being recalled.',
|
|
200
|
+
'',
|
|
201
|
+
'Never write credentials, keys, tokens, or cookies into memory — record only where',
|
|
202
|
+
'the secret lives.',
|
|
203
|
+
].join('\n')
|
|
204
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-engramory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one git-versioned store shared across hosts, and a deterministic index cap enforced by ctx.tools.guard()",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"dsh",
|
|
9
|
+
"memory",
|
|
10
|
+
"agent-memory",
|
|
11
|
+
"long-term-memory",
|
|
12
|
+
"engramory",
|
|
13
|
+
"markdown"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./index.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/tinqiao-oss/engramory.git",
|
|
30
|
+
"directory": "adapters/dsh/plugin"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/tinqiao-oss/engramory/tree/master/adapters/dsh#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/tinqiao-oss/engramory/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@deepseek-ai/dsh-tools": {
|
|
44
|
+
"optional": false
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|