@modusensus/dsh-mneme 0.1.3 → 0.1.4
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 +13 -3
- package/lib/api.js +105 -8
- package/lib/client.js +202 -3
- package/lib/commands.js +64 -0
- package/lib/index.js +22 -3
- package/lib/inject.js +33 -8
- package/lib/settings.js +120 -0
- package/package.json +1 -1
- package/src/api.js +105 -8
- package/src/commands.js +64 -0
- package/src/index.js +22 -3
- package/src/inject.js +33 -8
- package/src/settings.js +120 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
6
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
7
7
|
|
|
8
8
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
9
9
|
|
|
@@ -47,6 +47,16 @@
|
|
|
47
47
|
|
|
48
48
|
侧边栏"记忆"按钮 → 模态面板:按类型浏览、全文搜索、查看详情。
|
|
49
49
|
|
|
50
|
+
### 用户设置(画像 / 规则)与自定义指令 ⚙️
|
|
51
|
+
|
|
52
|
+
侧边栏"设置"按钮 → 设置面板:
|
|
53
|
+
|
|
54
|
+
- **用户画像**:一段自由文本描述用户自己(角色、背景、偏好),**每轮注入**到系统提示,让 Agent 始终遵循
|
|
55
|
+
- **规则**:Agent 必须遵守的行为规则列表(如"回答先给结论"),同样每轮注入
|
|
56
|
+
- **自定义指令**:注册斜杠命令(`/名称`),触发时把用户定义的指令内容交给 Agent。命令持久化到 SQLite,启动时自动注册到 DSH 命令表,增删实时生效
|
|
57
|
+
|
|
58
|
+
> 画像与规则通过独立的 `[用户设置]` 注入区块(优先级高于记忆库),即使记忆为空也会注入。
|
|
59
|
+
|
|
50
60
|
## 📦 安装
|
|
51
61
|
|
|
52
62
|
### 前置条件
|
|
@@ -150,7 +160,7 @@ src/
|
|
|
150
160
|
lib/
|
|
151
161
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
152
162
|
└── *.js # src 的同步分发产物
|
|
153
|
-
test/ #
|
|
163
|
+
test/ # 129 个 node:test 测试
|
|
154
164
|
```
|
|
155
165
|
|
|
156
166
|
## 🧪 开发
|
|
@@ -158,7 +168,7 @@ test/ # 108 个 node:test 测试
|
|
|
158
168
|
```bash
|
|
159
169
|
cd dsh-mneme
|
|
160
170
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
161
|
-
npm test # 运行
|
|
171
|
+
npm test # 运行 129 个测试(--test-isolation=none 用于受限沙箱,禁止子进程 spawn)
|
|
162
172
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
163
173
|
```
|
|
164
174
|
|
package/lib/api.js
CHANGED
|
@@ -5,19 +5,41 @@ function sendJson(res, status, payload) {
|
|
|
5
5
|
res.end(JSON.stringify(payload));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Collect the request body as text (tolerant of empty/invalid bodies). */
|
|
9
|
+
function readBody(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let body = "";
|
|
12
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
13
|
+
req.on("end", () => resolve(body));
|
|
14
|
+
req.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBody(text) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(text || "{}");
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createApi(ctx, service, settings, commands) {
|
|
9
27
|
const disposers = [];
|
|
10
28
|
|
|
29
|
+
const register = (route) => {
|
|
30
|
+
disposers.push(ctx.webServer.register(route));
|
|
31
|
+
};
|
|
32
|
+
|
|
11
33
|
// /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
|
|
12
|
-
|
|
34
|
+
register({
|
|
13
35
|
kind: "prefix",
|
|
14
36
|
path: "/api/dsh-mneme",
|
|
15
37
|
handler(req, res) {
|
|
16
38
|
sendJson(res, 404, { error: "not-found" });
|
|
17
39
|
}
|
|
18
|
-
})
|
|
40
|
+
});
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
register({
|
|
21
43
|
kind: "exact",
|
|
22
44
|
path: "/api/dsh-mneme/list",
|
|
23
45
|
handler(req, res) {
|
|
@@ -32,9 +54,9 @@ export function createApi(ctx, service) {
|
|
|
32
54
|
sendJson(res, 500, { error: "internal" });
|
|
33
55
|
}
|
|
34
56
|
}
|
|
35
|
-
})
|
|
57
|
+
});
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
register({
|
|
38
60
|
kind: "exact",
|
|
39
61
|
path: "/api/dsh-mneme/search",
|
|
40
62
|
handler(req, res) {
|
|
@@ -48,10 +70,85 @@ export function createApi(ctx, service) {
|
|
|
48
70
|
sendJson(res, 500, { error: "internal" });
|
|
49
71
|
}
|
|
50
72
|
}
|
|
51
|
-
})
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- user profile ---
|
|
76
|
+
register({
|
|
77
|
+
kind: "exact",
|
|
78
|
+
path: "/api/dsh-mneme/profile",
|
|
79
|
+
handler(req, res) {
|
|
80
|
+
try {
|
|
81
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
82
|
+
return readBody(req).then((text) => {
|
|
83
|
+
const body = parseBody(text);
|
|
84
|
+
settings.setProfile(typeof body.profile === "string" ? body.profile : "");
|
|
85
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
89
|
+
} catch {
|
|
90
|
+
sendJson(res, 500, { error: "internal" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- rules ---
|
|
96
|
+
register({
|
|
97
|
+
kind: "exact",
|
|
98
|
+
path: "/api/dsh-mneme/rules",
|
|
99
|
+
handler(req, res) {
|
|
100
|
+
try {
|
|
101
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
102
|
+
return readBody(req).then((text) => {
|
|
103
|
+
const body = parseBody(text);
|
|
104
|
+
settings.setRules(Array.isArray(body.rules) ? body.rules : []);
|
|
105
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
109
|
+
} catch {
|
|
110
|
+
sendJson(res, 500, { error: "internal" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// --- custom commands ---
|
|
116
|
+
register({
|
|
117
|
+
kind: "exact",
|
|
118
|
+
path: "/api/dsh-mneme/commands",
|
|
119
|
+
handler(req, res) {
|
|
120
|
+
try {
|
|
121
|
+
if (req.method === "POST") {
|
|
122
|
+
return readBody(req).then((text) => {
|
|
123
|
+
const body = parseBody(text);
|
|
124
|
+
try {
|
|
125
|
+
const command = commands.add({
|
|
126
|
+
name: body.name,
|
|
127
|
+
description: body.description,
|
|
128
|
+
instruction: body.instruction
|
|
129
|
+
});
|
|
130
|
+
sendJson(res, 200, { command });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
sendJson(res, 400, { error: error.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (req.method === "DELETE") {
|
|
137
|
+
const url = new URL(req.url, "http://localhost");
|
|
138
|
+
const id = url.searchParams.get("id");
|
|
139
|
+
const removed = id ? commands.remove(id) : false;
|
|
140
|
+
sendJson(res, 200, { removed });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sendJson(res, 200, { commands: commands.list() });
|
|
144
|
+
} catch {
|
|
145
|
+
sendJson(res, 500, { error: "internal" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
52
149
|
|
|
53
150
|
return {
|
|
54
|
-
routes:
|
|
151
|
+
routes: 6,
|
|
55
152
|
dispose: () => {
|
|
56
153
|
for (const dispose of disposers) dispose();
|
|
57
154
|
}
|
package/lib/client.js
CHANGED
|
@@ -23,7 +23,25 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
"memory.tab.preference": "偏好",
|
|
24
24
|
"memory.tab.project": "项目",
|
|
25
25
|
"memory.tab.decision": "决策",
|
|
26
|
-
"memory.tab.history": "历史"
|
|
26
|
+
"memory.tab.history": "历史",
|
|
27
|
+
"memory.settings.open": "设置",
|
|
28
|
+
"memory.settings.title": "记忆库设置",
|
|
29
|
+
"memory.settings.profile": "用户画像",
|
|
30
|
+
"memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
|
|
31
|
+
"memory.settings.profileSave": "保存画像",
|
|
32
|
+
"memory.settings.profileSaved": "画像已保存",
|
|
33
|
+
"memory.settings.rules": "规则",
|
|
34
|
+
"memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
|
|
35
|
+
"memory.settings.ruleAdd": "添加规则",
|
|
36
|
+
"memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
|
|
37
|
+
"memory.settings.commands": "自定义指令",
|
|
38
|
+
"memory.settings.commandsHint": "注册斜杠命令(/名称),触发时把指令内容交给 Agent",
|
|
39
|
+
"memory.settings.cmdName": "命令名",
|
|
40
|
+
"memory.settings.cmdDesc": "描述",
|
|
41
|
+
"memory.settings.cmdInstruction": "指令内容",
|
|
42
|
+
"memory.settings.cmdAdd": "添加命令",
|
|
43
|
+
"memory.settings.cmdDelete": "删除",
|
|
44
|
+
"memory.settings.empty": "暂无内容"
|
|
27
45
|
},
|
|
28
46
|
en: {
|
|
29
47
|
"memory.panel.title": "Memory",
|
|
@@ -34,7 +52,25 @@ window.__ModuleLoader__.load({
|
|
|
34
52
|
"memory.tab.preference": "Preferences",
|
|
35
53
|
"memory.tab.project": "Projects",
|
|
36
54
|
"memory.tab.decision": "Decisions",
|
|
37
|
-
"memory.tab.history": "History"
|
|
55
|
+
"memory.tab.history": "History",
|
|
56
|
+
"memory.settings.open": "Settings",
|
|
57
|
+
"memory.settings.title": "Memory Settings",
|
|
58
|
+
"memory.settings.profile": "User Profile",
|
|
59
|
+
"memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
|
|
60
|
+
"memory.settings.profileSave": "Save Profile",
|
|
61
|
+
"memory.settings.profileSaved": "Profile saved",
|
|
62
|
+
"memory.settings.rules": "Rules",
|
|
63
|
+
"memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
|
|
64
|
+
"memory.settings.ruleAdd": "Add Rule",
|
|
65
|
+
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
66
|
+
"memory.settings.commands": "Custom Commands",
|
|
67
|
+
"memory.settings.commandsHint": "Register slash commands (/name) whose instruction is handed to the agent",
|
|
68
|
+
"memory.settings.cmdName": "Name",
|
|
69
|
+
"memory.settings.cmdDesc": "Description",
|
|
70
|
+
"memory.settings.cmdInstruction": "Instruction",
|
|
71
|
+
"memory.settings.cmdAdd": "Add Command",
|
|
72
|
+
"memory.settings.cmdDelete": "Delete",
|
|
73
|
+
"memory.settings.empty": "Nothing yet"
|
|
38
74
|
}
|
|
39
75
|
};
|
|
40
76
|
|
|
@@ -134,6 +170,163 @@ window.__ModuleLoader__.load({
|
|
|
134
170
|
);
|
|
135
171
|
}
|
|
136
172
|
|
|
173
|
+
const h = react.createElement;
|
|
174
|
+
|
|
175
|
+
// --- Settings panel: user profile, rules, custom commands ---
|
|
176
|
+
function SettingsPanel({ t, onClose }) {
|
|
177
|
+
const [profile, setProfile] = react.useState("");
|
|
178
|
+
const [rules, setRules] = react.useState([]);
|
|
179
|
+
const [commands, setCommands] = react.useState([]);
|
|
180
|
+
const [newRule, setNewRule] = react.useState("");
|
|
181
|
+
const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
|
|
182
|
+
const [saved, setSaved] = react.useState(false);
|
|
183
|
+
const [cmdError, setCmdError] = react.useState("");
|
|
184
|
+
|
|
185
|
+
const load = react.useCallback(async () => {
|
|
186
|
+
try {
|
|
187
|
+
const [p, r, c] = await Promise.all([
|
|
188
|
+
fetch("/api/dsh-mneme/profile").then((res) => res.json()),
|
|
189
|
+
fetch("/api/dsh-mneme/rules").then((res) => res.json()),
|
|
190
|
+
fetch("/api/dsh-mneme/commands").then((res) => res.json())
|
|
191
|
+
]);
|
|
192
|
+
setProfile(p.profile || "");
|
|
193
|
+
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
194
|
+
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
195
|
+
} catch { /* ignore */ }
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
react.useEffect(() => { load(); }, [load]);
|
|
199
|
+
|
|
200
|
+
async function saveProfile() {
|
|
201
|
+
try {
|
|
202
|
+
await fetch("/api/dsh-mneme/profile", {
|
|
203
|
+
method: "PUT",
|
|
204
|
+
headers: { "Content-Type": "application/json" },
|
|
205
|
+
body: JSON.stringify({ profile })
|
|
206
|
+
});
|
|
207
|
+
setSaved(true);
|
|
208
|
+
setTimeout(() => setSaved(false), 1500);
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function putRules(next) {
|
|
213
|
+
await fetch("/api/dsh-mneme/rules", {
|
|
214
|
+
method: "PUT",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({ rules: next })
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function addRule() {
|
|
221
|
+
const text = newRule.trim();
|
|
222
|
+
if (!text) return;
|
|
223
|
+
const next = [...rules, text];
|
|
224
|
+
await putRules(next);
|
|
225
|
+
setRules(next);
|
|
226
|
+
setNewRule("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function removeRule(index) {
|
|
230
|
+
const next = rules.filter((_, i) => i !== index);
|
|
231
|
+
await putRules(next);
|
|
232
|
+
setRules(next);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function addCommand() {
|
|
236
|
+
const name = newCmd.name.trim();
|
|
237
|
+
const instruction = newCmd.instruction.trim();
|
|
238
|
+
if (!name || !instruction) return;
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch("/api/dsh-mneme/commands", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({ name, description: newCmd.description, instruction })
|
|
244
|
+
});
|
|
245
|
+
const data = await res.json();
|
|
246
|
+
if (!res.ok) { setCmdError(data.error || "failed"); return; }
|
|
247
|
+
setCmdError("");
|
|
248
|
+
setCommands([...commands, data.command]);
|
|
249
|
+
setNewCmd({ name: "", description: "", instruction: "" });
|
|
250
|
+
} catch { setCmdError("failed"); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function removeCommand(id) {
|
|
254
|
+
await fetch(`/api/dsh-mneme/commands?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
255
|
+
setCommands(commands.filter((c) => c.id !== id));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const inputStyle = { ...styles.search, marginBottom: 8 };
|
|
259
|
+
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px" };
|
|
260
|
+
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
261
|
+
const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
|
|
262
|
+
|
|
263
|
+
return createPortal(
|
|
264
|
+
h("div", { style: styles.overlay },
|
|
265
|
+
h("div", { style: styles.panel },
|
|
266
|
+
h("div", { style: styles.header },
|
|
267
|
+
h("span", { style: styles.title }, t("memory.settings.title")),
|
|
268
|
+
h("button", { style: styles.close, onClick: onClose }, "×")
|
|
269
|
+
),
|
|
270
|
+
h("div", { style: { overflowY: "auto" } },
|
|
271
|
+
// profile
|
|
272
|
+
h("div", { style: labelStyle }, t("memory.settings.profile")),
|
|
273
|
+
h("div", { style: hintStyle }, t("memory.settings.profileHint")),
|
|
274
|
+
h("textarea", {
|
|
275
|
+
style: { ...inputStyle, minHeight: 72, resize: "vertical", fontFamily: "inherit" },
|
|
276
|
+
value: profile,
|
|
277
|
+
placeholder: t("memory.settings.profile"),
|
|
278
|
+
onChange: (e) => setProfile(e.target.value)
|
|
279
|
+
}),
|
|
280
|
+
h("div", null,
|
|
281
|
+
h("button", { style: styles.footerButton, onClick: saveProfile }, t("memory.settings.profileSave")),
|
|
282
|
+
saved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.profileSaved"))
|
|
283
|
+
),
|
|
284
|
+
// rules
|
|
285
|
+
h("div", { style: labelStyle }, t("memory.settings.rules")),
|
|
286
|
+
h("div", { style: hintStyle }, t("memory.settings.rulesHint")),
|
|
287
|
+
rules.length === 0 && h("div", { style: styles.hint }, t("memory.settings.empty")),
|
|
288
|
+
rules.map((rule, i) =>
|
|
289
|
+
h("div", { key: i, style: rowStyle },
|
|
290
|
+
h("span", { style: { fontSize: 13, flex: 1 } }, rule),
|
|
291
|
+
h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeRule(i) }, "×")
|
|
292
|
+
)
|
|
293
|
+
),
|
|
294
|
+
h("div", { style: { display: "flex", gap: 6 } },
|
|
295
|
+
h("input", {
|
|
296
|
+
style: { ...inputStyle, flex: 1, marginBottom: 0 },
|
|
297
|
+
value: newRule,
|
|
298
|
+
placeholder: t("memory.settings.rulePlaceholder"),
|
|
299
|
+
onChange: (e) => setNewRule(e.target.value)
|
|
300
|
+
}),
|
|
301
|
+
h("button", { style: styles.footerButton, onClick: addRule }, t("memory.settings.ruleAdd"))
|
|
302
|
+
),
|
|
303
|
+
// custom commands
|
|
304
|
+
h("div", { style: labelStyle }, t("memory.settings.commands")),
|
|
305
|
+
h("div", { style: hintStyle }, t("memory.settings.commandsHint")),
|
|
306
|
+
commands.length === 0 && h("div", { style: styles.hint }, t("memory.settings.empty")),
|
|
307
|
+
commands.map((cmd) =>
|
|
308
|
+
h("div", { key: cmd.id, style: rowStyle },
|
|
309
|
+
h("div", { style: { flex: 1 } },
|
|
310
|
+
h("div", { style: { fontSize: 13, fontWeight: 600 } }, `/${cmd.name}`),
|
|
311
|
+
h("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" } }, cmd.description || cmd.instruction)
|
|
312
|
+
),
|
|
313
|
+
h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeCommand(cmd.id) }, t("memory.settings.cmdDelete"))
|
|
314
|
+
)
|
|
315
|
+
),
|
|
316
|
+
h("div", { style: { display: "grid", gap: 6, marginTop: 6 } },
|
|
317
|
+
h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
|
|
318
|
+
h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) }),
|
|
319
|
+
h("textarea", { style: { ...inputStyle, marginBottom: 0, minHeight: 48, resize: "vertical", fontFamily: "inherit" }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
|
|
320
|
+
h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
|
|
321
|
+
cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
),
|
|
326
|
+
document.body
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
137
330
|
const styles = {
|
|
138
331
|
overlay: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" },
|
|
139
332
|
panel: { background: "var(--dsw-alias-bg-base, #fff)", borderRadius: 12, width: 640, maxWidth: "90vw", maxHeight: "80vh", display: "flex", flexDirection: "column", padding: 16, boxShadow: "0 8px 40px rgba(0,0,0,0.2)" },
|
|
@@ -168,12 +361,18 @@ window.__ModuleLoader__.load({
|
|
|
168
361
|
inject: () => ({})
|
|
169
362
|
}, () => {
|
|
170
363
|
const [open, setOpen] = react.useState(false);
|
|
364
|
+
const [openSettings, setOpenSettings] = react.useState(false);
|
|
171
365
|
return react.createElement(react.Fragment, null,
|
|
172
366
|
react.createElement("button", {
|
|
173
367
|
onClick: () => setOpen(true),
|
|
174
368
|
style: { ...styles.footerButton, ...(open ? styles.footerButtonActive : {}) }
|
|
175
369
|
}, t("memory.panel.open")),
|
|
176
|
-
|
|
370
|
+
react.createElement("button", {
|
|
371
|
+
onClick: () => setOpenSettings(true),
|
|
372
|
+
style: { ...styles.footerButton, ...(openSettings ? styles.footerButtonActive : {}) }
|
|
373
|
+
}, t("memory.settings.open")),
|
|
374
|
+
open && react.createElement(MemoryPanel, { t, onClose: () => setOpen(false) }),
|
|
375
|
+
openSettings && react.createElement(SettingsPanel, { t, onClose: () => setOpenSettings(false) })
|
|
177
376
|
);
|
|
178
377
|
})
|
|
179
378
|
);
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
2
|
+
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
3
|
+
// and (re)registered on add/remove through the API.
|
|
4
|
+
//
|
|
5
|
+
// Each custom command's handler returns the user-authored instruction as a
|
|
6
|
+
// success result; the DSH UI surfaces it as a model-directed instruction.
|
|
7
|
+
export function createCommandManager({ ctx, settings, logger }) {
|
|
8
|
+
const registered = new Map(); // name -> disposer
|
|
9
|
+
|
|
10
|
+
function registerOne(command) {
|
|
11
|
+
if (registered.has(command.name)) return;
|
|
12
|
+
let dispose;
|
|
13
|
+
try {
|
|
14
|
+
dispose = ctx.commands.register({
|
|
15
|
+
name: command.name,
|
|
16
|
+
description: command.description || `自定义指令 ${command.name}`,
|
|
17
|
+
handler: () => ({ kind: "success", text: command.instruction })
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
registered.set(command.name, dispose);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unregisterOne(name) {
|
|
27
|
+
const dispose = registered.get(name);
|
|
28
|
+
if (dispose) {
|
|
29
|
+
try {
|
|
30
|
+
dispose();
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore double-dispose */
|
|
33
|
+
}
|
|
34
|
+
registered.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register every stored command (boot-time sync). */
|
|
39
|
+
function sync() {
|
|
40
|
+
for (const command of settings.listCommands()) registerOne(command);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add (or replace) a command and register it live. */
|
|
44
|
+
function add({ name, description, instruction }) {
|
|
45
|
+
const command = settings.addCommand({ name, description, instruction });
|
|
46
|
+
registerOne(command);
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Remove a command by id and unregister it live. */
|
|
51
|
+
function remove(id) {
|
|
52
|
+
const existing = settings.listCommands().find((c) => c.id === id);
|
|
53
|
+
if (!existing) return false;
|
|
54
|
+
if (!settings.removeCommand(id)) return false;
|
|
55
|
+
unregisterOne(existing.name);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispose() {
|
|
60
|
+
for (const name of [...registered.keys()]) unregisterOne(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { sync, add, remove, list: () => settings.listCommands(), dispose };
|
|
64
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -6,13 +6,15 @@ import { createInjector } from "./inject.js";
|
|
|
6
6
|
import { createSummarizer } from "./summarize.js";
|
|
7
7
|
import { createDreamScheduler } from "./dream.js";
|
|
8
8
|
import { createApi } from "./api.js";
|
|
9
|
+
import { createSettings } from "./settings.js";
|
|
10
|
+
import { createCommandManager } from "./commands.js";
|
|
9
11
|
import { Config } from "./config.js";
|
|
10
12
|
import { mkdirSync } from "node:fs";
|
|
11
13
|
import { join } from "node:path";
|
|
12
14
|
import { homedir } from "node:os";
|
|
13
15
|
|
|
14
16
|
export const name = "dsh-mneme";
|
|
15
|
-
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel"];
|
|
17
|
+
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel", "commands"];
|
|
16
18
|
export { Config };
|
|
17
19
|
|
|
18
20
|
// Arrow (not function declaration): cordis 4 treats any apply with a
|
|
@@ -33,6 +35,18 @@ export const apply = (ctx, config) => {
|
|
|
33
35
|
const mirror = createMirror(memoryDir);
|
|
34
36
|
const service = createService({ store, mirror, config: cfg });
|
|
35
37
|
|
|
38
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
39
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
40
|
+
const settings = createSettings(store.db);
|
|
41
|
+
|
|
42
|
+
// Custom commands: register persisted commands into the DSH command registry
|
|
43
|
+
// on boot; add/remove re-register live through the API.
|
|
44
|
+
let commands = null;
|
|
45
|
+
if (ctx.commands) {
|
|
46
|
+
commands = createCommandManager({ ctx, settings, logger: ctx.logger });
|
|
47
|
+
commands.sync();
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
// Human edits in mirror files win on every sync; merge them back first.
|
|
37
51
|
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
38
52
|
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
@@ -67,7 +81,7 @@ export const apply = (ctx, config) => {
|
|
|
67
81
|
const disposers = [];
|
|
68
82
|
|
|
69
83
|
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
70
|
-
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, cfg));
|
|
84
|
+
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, settings, cfg));
|
|
71
85
|
});
|
|
72
86
|
|
|
73
87
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
@@ -78,7 +92,11 @@ export const apply = (ctx, config) => {
|
|
|
78
92
|
disposers.push(summarizer.dispose);
|
|
79
93
|
|
|
80
94
|
if (ctx.webServer) {
|
|
81
|
-
const api = createApi(ctx, service
|
|
95
|
+
const api = createApi(ctx, service, settings, commands ?? {
|
|
96
|
+
add: () => { throw new Error("commands unavailable"); },
|
|
97
|
+
remove: () => false,
|
|
98
|
+
list: () => []
|
|
99
|
+
});
|
|
82
100
|
disposers.push(api.dispose);
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -89,6 +107,7 @@ export const apply = (ctx, config) => {
|
|
|
89
107
|
for (const dispose of disposers) {
|
|
90
108
|
if (typeof dispose === "function") dispose();
|
|
91
109
|
}
|
|
110
|
+
commands?.dispose();
|
|
92
111
|
if (dream) await dream.dispose();
|
|
93
112
|
store.close();
|
|
94
113
|
};
|
package/lib/inject.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function createInjector(ctx, service, config) {
|
|
1
|
+
export function createInjector(ctx, service, settings, config) {
|
|
2
2
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
3
3
|
const threshold = config.importanceThreshold ?? 3;
|
|
4
4
|
|
|
@@ -11,12 +11,37 @@ export function createInjector(ctx, service, config) {
|
|
|
11
11
|
return lines.join("\n");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// User profile + rules: injected ahead of the memory block because they are
|
|
15
|
+
// always-relevant instructions the agent should follow every turn.
|
|
16
|
+
function renderUserSettings() {
|
|
17
|
+
const profile = settings.getProfile().trim();
|
|
18
|
+
const rules = settings.getRules();
|
|
19
|
+
if (!profile && !rules.length) return "";
|
|
20
|
+
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
21
|
+
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
22
|
+
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const disposers = [
|
|
27
|
+
ctx.systemPrompt.context({
|
|
28
|
+
name: "memory",
|
|
29
|
+
order: 90,
|
|
30
|
+
text: () => {
|
|
31
|
+
const candidates = service.injectCandidates({ maxItems, threshold });
|
|
32
|
+
return render(candidates);
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
ctx.systemPrompt.context({
|
|
36
|
+
name: "user-settings",
|
|
37
|
+
order: 85,
|
|
38
|
+
text: renderUserSettings
|
|
39
|
+
})
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
for (const dispose of disposers) {
|
|
44
|
+
if (typeof dispose === "function") dispose();
|
|
20
45
|
}
|
|
21
|
-
}
|
|
46
|
+
};
|
|
22
47
|
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// User-configurable settings: profile (user self-description), rules (behavior
|
|
2
|
+
// rules the agent must follow), and custom slash commands. Stored in the same
|
|
3
|
+
// SQLite database via dedicated tables, isolated from the memories store.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
8
|
+
key TEXT PRIMARY KEY,
|
|
9
|
+
value TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS custom_commands (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
name TEXT NOT NULL UNIQUE,
|
|
14
|
+
description TEXT NOT NULL DEFAULT '',
|
|
15
|
+
instruction TEXT NOT NULL,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
// DSH command names must match this (lowercase, start with a letter).
|
|
22
|
+
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
23
|
+
|
|
24
|
+
/** Parse a JSON array out of a stored string, tolerant of corruption. */
|
|
25
|
+
function parseList(raw) {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(raw);
|
|
28
|
+
return Array.isArray(value) ? value : [];
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSettings(db) {
|
|
35
|
+
db.exec(SCHEMA);
|
|
36
|
+
|
|
37
|
+
function getSetting(key) {
|
|
38
|
+
const row = db.prepare("SELECT value FROM user_settings WHERE key = ?").get(key);
|
|
39
|
+
return row?.value ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setSetting(key, value) {
|
|
43
|
+
db.prepare(
|
|
44
|
+
`INSERT INTO user_settings (key, value) VALUES (?, ?)
|
|
45
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
46
|
+
).run(key, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toCommand(row) {
|
|
50
|
+
if (!row) return undefined;
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
name: row.name,
|
|
54
|
+
description: row.description,
|
|
55
|
+
instruction: row.instruction,
|
|
56
|
+
created_at: row.created_at,
|
|
57
|
+
updated_at: row.updated_at
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
/** The user's self-description (free text) or "" when unset. */
|
|
63
|
+
getProfile() {
|
|
64
|
+
return getSetting("profile") ?? "";
|
|
65
|
+
},
|
|
66
|
+
setProfile(text) {
|
|
67
|
+
setSetting("profile", String(text ?? ""));
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/** Behavior rules as an array of strings. */
|
|
71
|
+
getRules() {
|
|
72
|
+
return parseList(getSetting("rules") ?? "[]").filter((r) => typeof r === "string");
|
|
73
|
+
},
|
|
74
|
+
setRules(rules) {
|
|
75
|
+
const list = Array.isArray(rules) ? rules.filter((r) => typeof r === "string") : [];
|
|
76
|
+
setSetting("rules", JSON.stringify(list));
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** All custom commands, sorted by name. */
|
|
80
|
+
listCommands() {
|
|
81
|
+
const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();
|
|
82
|
+
return rows.map(toCommand);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Add or replace a custom command by name.
|
|
87
|
+
* @returns the stored command.
|
|
88
|
+
* @throws when name is invalid or does not match DSH's command-name grammar.
|
|
89
|
+
*/
|
|
90
|
+
addCommand({ name, description = "", instruction }) {
|
|
91
|
+
const cmdName = String(name ?? "").trim();
|
|
92
|
+
if (!COMMAND_NAME.test(cmdName)) {
|
|
93
|
+
throw new Error(`invalid command name "${cmdName}": must match /^[a-z][a-z0-9_-]*$/`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof instruction !== "string" || !instruction.trim()) {
|
|
96
|
+
throw new Error("command instruction must be a non-empty string");
|
|
97
|
+
}
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const existing = db.prepare("SELECT id FROM custom_commands WHERE name = ?").get(cmdName);
|
|
100
|
+
if (existing) {
|
|
101
|
+
db.prepare(
|
|
102
|
+
"UPDATE custom_commands SET description = ?, instruction = ?, updated_at = ? WHERE id = ?"
|
|
103
|
+
).run(String(description ?? ""), instruction, now, existing.id);
|
|
104
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(existing.id));
|
|
105
|
+
}
|
|
106
|
+
const id = randomUUID();
|
|
107
|
+
db.prepare(
|
|
108
|
+
`INSERT INTO custom_commands (id, name, description, instruction, created_at, updated_at)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
110
|
+
).run(id, cmdName, String(description ?? ""), instruction, now, now);
|
|
111
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(id));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Remove a custom command by id; returns true when removed. */
|
|
115
|
+
removeCommand(id) {
|
|
116
|
+
const result = db.prepare("DELETE FROM custom_commands WHERE id = ?").run(id);
|
|
117
|
+
return result.changes > 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 6 model tools, automatic injection, session summarization, and a Web GUI panel",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/src/api.js
CHANGED
|
@@ -5,19 +5,41 @@ function sendJson(res, status, payload) {
|
|
|
5
5
|
res.end(JSON.stringify(payload));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Collect the request body as text (tolerant of empty/invalid bodies). */
|
|
9
|
+
function readBody(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let body = "";
|
|
12
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
13
|
+
req.on("end", () => resolve(body));
|
|
14
|
+
req.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBody(text) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(text || "{}");
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createApi(ctx, service, settings, commands) {
|
|
9
27
|
const disposers = [];
|
|
10
28
|
|
|
29
|
+
const register = (route) => {
|
|
30
|
+
disposers.push(ctx.webServer.register(route));
|
|
31
|
+
};
|
|
32
|
+
|
|
11
33
|
// /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
|
|
12
|
-
|
|
34
|
+
register({
|
|
13
35
|
kind: "prefix",
|
|
14
36
|
path: "/api/dsh-mneme",
|
|
15
37
|
handler(req, res) {
|
|
16
38
|
sendJson(res, 404, { error: "not-found" });
|
|
17
39
|
}
|
|
18
|
-
})
|
|
40
|
+
});
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
register({
|
|
21
43
|
kind: "exact",
|
|
22
44
|
path: "/api/dsh-mneme/list",
|
|
23
45
|
handler(req, res) {
|
|
@@ -32,9 +54,9 @@ export function createApi(ctx, service) {
|
|
|
32
54
|
sendJson(res, 500, { error: "internal" });
|
|
33
55
|
}
|
|
34
56
|
}
|
|
35
|
-
})
|
|
57
|
+
});
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
register({
|
|
38
60
|
kind: "exact",
|
|
39
61
|
path: "/api/dsh-mneme/search",
|
|
40
62
|
handler(req, res) {
|
|
@@ -48,10 +70,85 @@ export function createApi(ctx, service) {
|
|
|
48
70
|
sendJson(res, 500, { error: "internal" });
|
|
49
71
|
}
|
|
50
72
|
}
|
|
51
|
-
})
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- user profile ---
|
|
76
|
+
register({
|
|
77
|
+
kind: "exact",
|
|
78
|
+
path: "/api/dsh-mneme/profile",
|
|
79
|
+
handler(req, res) {
|
|
80
|
+
try {
|
|
81
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
82
|
+
return readBody(req).then((text) => {
|
|
83
|
+
const body = parseBody(text);
|
|
84
|
+
settings.setProfile(typeof body.profile === "string" ? body.profile : "");
|
|
85
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
89
|
+
} catch {
|
|
90
|
+
sendJson(res, 500, { error: "internal" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- rules ---
|
|
96
|
+
register({
|
|
97
|
+
kind: "exact",
|
|
98
|
+
path: "/api/dsh-mneme/rules",
|
|
99
|
+
handler(req, res) {
|
|
100
|
+
try {
|
|
101
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
102
|
+
return readBody(req).then((text) => {
|
|
103
|
+
const body = parseBody(text);
|
|
104
|
+
settings.setRules(Array.isArray(body.rules) ? body.rules : []);
|
|
105
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
109
|
+
} catch {
|
|
110
|
+
sendJson(res, 500, { error: "internal" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// --- custom commands ---
|
|
116
|
+
register({
|
|
117
|
+
kind: "exact",
|
|
118
|
+
path: "/api/dsh-mneme/commands",
|
|
119
|
+
handler(req, res) {
|
|
120
|
+
try {
|
|
121
|
+
if (req.method === "POST") {
|
|
122
|
+
return readBody(req).then((text) => {
|
|
123
|
+
const body = parseBody(text);
|
|
124
|
+
try {
|
|
125
|
+
const command = commands.add({
|
|
126
|
+
name: body.name,
|
|
127
|
+
description: body.description,
|
|
128
|
+
instruction: body.instruction
|
|
129
|
+
});
|
|
130
|
+
sendJson(res, 200, { command });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
sendJson(res, 400, { error: error.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (req.method === "DELETE") {
|
|
137
|
+
const url = new URL(req.url, "http://localhost");
|
|
138
|
+
const id = url.searchParams.get("id");
|
|
139
|
+
const removed = id ? commands.remove(id) : false;
|
|
140
|
+
sendJson(res, 200, { removed });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sendJson(res, 200, { commands: commands.list() });
|
|
144
|
+
} catch {
|
|
145
|
+
sendJson(res, 500, { error: "internal" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
52
149
|
|
|
53
150
|
return {
|
|
54
|
-
routes:
|
|
151
|
+
routes: 6,
|
|
55
152
|
dispose: () => {
|
|
56
153
|
for (const dispose of disposers) dispose();
|
|
57
154
|
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
2
|
+
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
3
|
+
// and (re)registered on add/remove through the API.
|
|
4
|
+
//
|
|
5
|
+
// Each custom command's handler returns the user-authored instruction as a
|
|
6
|
+
// success result; the DSH UI surfaces it as a model-directed instruction.
|
|
7
|
+
export function createCommandManager({ ctx, settings, logger }) {
|
|
8
|
+
const registered = new Map(); // name -> disposer
|
|
9
|
+
|
|
10
|
+
function registerOne(command) {
|
|
11
|
+
if (registered.has(command.name)) return;
|
|
12
|
+
let dispose;
|
|
13
|
+
try {
|
|
14
|
+
dispose = ctx.commands.register({
|
|
15
|
+
name: command.name,
|
|
16
|
+
description: command.description || `自定义指令 ${command.name}`,
|
|
17
|
+
handler: () => ({ kind: "success", text: command.instruction })
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
registered.set(command.name, dispose);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unregisterOne(name) {
|
|
27
|
+
const dispose = registered.get(name);
|
|
28
|
+
if (dispose) {
|
|
29
|
+
try {
|
|
30
|
+
dispose();
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore double-dispose */
|
|
33
|
+
}
|
|
34
|
+
registered.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register every stored command (boot-time sync). */
|
|
39
|
+
function sync() {
|
|
40
|
+
for (const command of settings.listCommands()) registerOne(command);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add (or replace) a command and register it live. */
|
|
44
|
+
function add({ name, description, instruction }) {
|
|
45
|
+
const command = settings.addCommand({ name, description, instruction });
|
|
46
|
+
registerOne(command);
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Remove a command by id and unregister it live. */
|
|
51
|
+
function remove(id) {
|
|
52
|
+
const existing = settings.listCommands().find((c) => c.id === id);
|
|
53
|
+
if (!existing) return false;
|
|
54
|
+
if (!settings.removeCommand(id)) return false;
|
|
55
|
+
unregisterOne(existing.name);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispose() {
|
|
60
|
+
for (const name of [...registered.keys()]) unregisterOne(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { sync, add, remove, list: () => settings.listCommands(), dispose };
|
|
64
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,13 +6,15 @@ import { createInjector } from "./inject.js";
|
|
|
6
6
|
import { createSummarizer } from "./summarize.js";
|
|
7
7
|
import { createDreamScheduler } from "./dream.js";
|
|
8
8
|
import { createApi } from "./api.js";
|
|
9
|
+
import { createSettings } from "./settings.js";
|
|
10
|
+
import { createCommandManager } from "./commands.js";
|
|
9
11
|
import { Config } from "./config.js";
|
|
10
12
|
import { mkdirSync } from "node:fs";
|
|
11
13
|
import { join } from "node:path";
|
|
12
14
|
import { homedir } from "node:os";
|
|
13
15
|
|
|
14
16
|
export const name = "dsh-mneme";
|
|
15
|
-
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel"];
|
|
17
|
+
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel", "commands"];
|
|
16
18
|
export { Config };
|
|
17
19
|
|
|
18
20
|
// Arrow (not function declaration): cordis 4 treats any apply with a
|
|
@@ -33,6 +35,18 @@ export const apply = (ctx, config) => {
|
|
|
33
35
|
const mirror = createMirror(memoryDir);
|
|
34
36
|
const service = createService({ store, mirror, config: cfg });
|
|
35
37
|
|
|
38
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
39
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
40
|
+
const settings = createSettings(store.db);
|
|
41
|
+
|
|
42
|
+
// Custom commands: register persisted commands into the DSH command registry
|
|
43
|
+
// on boot; add/remove re-register live through the API.
|
|
44
|
+
let commands = null;
|
|
45
|
+
if (ctx.commands) {
|
|
46
|
+
commands = createCommandManager({ ctx, settings, logger: ctx.logger });
|
|
47
|
+
commands.sync();
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
// Human edits in mirror files win on every sync; merge them back first.
|
|
37
51
|
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
38
52
|
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
@@ -67,7 +81,7 @@ export const apply = (ctx, config) => {
|
|
|
67
81
|
const disposers = [];
|
|
68
82
|
|
|
69
83
|
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
70
|
-
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, cfg));
|
|
84
|
+
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, settings, cfg));
|
|
71
85
|
});
|
|
72
86
|
|
|
73
87
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
@@ -78,7 +92,11 @@ export const apply = (ctx, config) => {
|
|
|
78
92
|
disposers.push(summarizer.dispose);
|
|
79
93
|
|
|
80
94
|
if (ctx.webServer) {
|
|
81
|
-
const api = createApi(ctx, service
|
|
95
|
+
const api = createApi(ctx, service, settings, commands ?? {
|
|
96
|
+
add: () => { throw new Error("commands unavailable"); },
|
|
97
|
+
remove: () => false,
|
|
98
|
+
list: () => []
|
|
99
|
+
});
|
|
82
100
|
disposers.push(api.dispose);
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -89,6 +107,7 @@ export const apply = (ctx, config) => {
|
|
|
89
107
|
for (const dispose of disposers) {
|
|
90
108
|
if (typeof dispose === "function") dispose();
|
|
91
109
|
}
|
|
110
|
+
commands?.dispose();
|
|
92
111
|
if (dream) await dream.dispose();
|
|
93
112
|
store.close();
|
|
94
113
|
};
|
package/src/inject.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function createInjector(ctx, service, config) {
|
|
1
|
+
export function createInjector(ctx, service, settings, config) {
|
|
2
2
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
3
3
|
const threshold = config.importanceThreshold ?? 3;
|
|
4
4
|
|
|
@@ -11,12 +11,37 @@ export function createInjector(ctx, service, config) {
|
|
|
11
11
|
return lines.join("\n");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// User profile + rules: injected ahead of the memory block because they are
|
|
15
|
+
// always-relevant instructions the agent should follow every turn.
|
|
16
|
+
function renderUserSettings() {
|
|
17
|
+
const profile = settings.getProfile().trim();
|
|
18
|
+
const rules = settings.getRules();
|
|
19
|
+
if (!profile && !rules.length) return "";
|
|
20
|
+
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
21
|
+
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
22
|
+
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const disposers = [
|
|
27
|
+
ctx.systemPrompt.context({
|
|
28
|
+
name: "memory",
|
|
29
|
+
order: 90,
|
|
30
|
+
text: () => {
|
|
31
|
+
const candidates = service.injectCandidates({ maxItems, threshold });
|
|
32
|
+
return render(candidates);
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
ctx.systemPrompt.context({
|
|
36
|
+
name: "user-settings",
|
|
37
|
+
order: 85,
|
|
38
|
+
text: renderUserSettings
|
|
39
|
+
})
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
for (const dispose of disposers) {
|
|
44
|
+
if (typeof dispose === "function") dispose();
|
|
20
45
|
}
|
|
21
|
-
}
|
|
46
|
+
};
|
|
22
47
|
}
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// User-configurable settings: profile (user self-description), rules (behavior
|
|
2
|
+
// rules the agent must follow), and custom slash commands. Stored in the same
|
|
3
|
+
// SQLite database via dedicated tables, isolated from the memories store.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
8
|
+
key TEXT PRIMARY KEY,
|
|
9
|
+
value TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS custom_commands (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
name TEXT NOT NULL UNIQUE,
|
|
14
|
+
description TEXT NOT NULL DEFAULT '',
|
|
15
|
+
instruction TEXT NOT NULL,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
// DSH command names must match this (lowercase, start with a letter).
|
|
22
|
+
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
23
|
+
|
|
24
|
+
/** Parse a JSON array out of a stored string, tolerant of corruption. */
|
|
25
|
+
function parseList(raw) {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(raw);
|
|
28
|
+
return Array.isArray(value) ? value : [];
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSettings(db) {
|
|
35
|
+
db.exec(SCHEMA);
|
|
36
|
+
|
|
37
|
+
function getSetting(key) {
|
|
38
|
+
const row = db.prepare("SELECT value FROM user_settings WHERE key = ?").get(key);
|
|
39
|
+
return row?.value ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setSetting(key, value) {
|
|
43
|
+
db.prepare(
|
|
44
|
+
`INSERT INTO user_settings (key, value) VALUES (?, ?)
|
|
45
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
46
|
+
).run(key, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toCommand(row) {
|
|
50
|
+
if (!row) return undefined;
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
name: row.name,
|
|
54
|
+
description: row.description,
|
|
55
|
+
instruction: row.instruction,
|
|
56
|
+
created_at: row.created_at,
|
|
57
|
+
updated_at: row.updated_at
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
/** The user's self-description (free text) or "" when unset. */
|
|
63
|
+
getProfile() {
|
|
64
|
+
return getSetting("profile") ?? "";
|
|
65
|
+
},
|
|
66
|
+
setProfile(text) {
|
|
67
|
+
setSetting("profile", String(text ?? ""));
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/** Behavior rules as an array of strings. */
|
|
71
|
+
getRules() {
|
|
72
|
+
return parseList(getSetting("rules") ?? "[]").filter((r) => typeof r === "string");
|
|
73
|
+
},
|
|
74
|
+
setRules(rules) {
|
|
75
|
+
const list = Array.isArray(rules) ? rules.filter((r) => typeof r === "string") : [];
|
|
76
|
+
setSetting("rules", JSON.stringify(list));
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** All custom commands, sorted by name. */
|
|
80
|
+
listCommands() {
|
|
81
|
+
const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();
|
|
82
|
+
return rows.map(toCommand);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Add or replace a custom command by name.
|
|
87
|
+
* @returns the stored command.
|
|
88
|
+
* @throws when name is invalid or does not match DSH's command-name grammar.
|
|
89
|
+
*/
|
|
90
|
+
addCommand({ name, description = "", instruction }) {
|
|
91
|
+
const cmdName = String(name ?? "").trim();
|
|
92
|
+
if (!COMMAND_NAME.test(cmdName)) {
|
|
93
|
+
throw new Error(`invalid command name "${cmdName}": must match /^[a-z][a-z0-9_-]*$/`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof instruction !== "string" || !instruction.trim()) {
|
|
96
|
+
throw new Error("command instruction must be a non-empty string");
|
|
97
|
+
}
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const existing = db.prepare("SELECT id FROM custom_commands WHERE name = ?").get(cmdName);
|
|
100
|
+
if (existing) {
|
|
101
|
+
db.prepare(
|
|
102
|
+
"UPDATE custom_commands SET description = ?, instruction = ?, updated_at = ? WHERE id = ?"
|
|
103
|
+
).run(String(description ?? ""), instruction, now, existing.id);
|
|
104
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(existing.id));
|
|
105
|
+
}
|
|
106
|
+
const id = randomUUID();
|
|
107
|
+
db.prepare(
|
|
108
|
+
`INSERT INTO custom_commands (id, name, description, instruction, created_at, updated_at)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
110
|
+
).run(id, cmdName, String(description ?? ""), instruction, now, now);
|
|
111
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(id));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Remove a custom command by id; returns true when removed. */
|
|
115
|
+
removeCommand(id) {
|
|
116
|
+
const result = db.prepare("DELETE FROM custom_commands WHERE id = ?").run(id);
|
|
117
|
+
return result.changes > 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|