dsh-hub-reminders 0.0.0-stage → 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/cordis.patch.yml +33 -0
- package/package.json +19 -3
- package/plugin.js +244 -0
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: ai-agent-hub-reminders
|
|
3
|
+
name: 'dsh-hub-reminders'
|
|
4
|
+
config:
|
|
5
|
+
hooksDir: C:/Users/yh109/Documents/GitHub/AI-Agent-Hub/hooks
|
|
6
|
+
timeoutMs: 30000
|
|
7
|
+
maxBufferBytes: 1048576
|
|
8
|
+
scripts:
|
|
9
|
+
- session_memory_check.py
|
|
10
|
+
- report_navigation_reminder.py
|
|
11
|
+
- hpc_parallel_check.py
|
|
12
|
+
- memory_update_reminder.py
|
|
13
|
+
- drawing_standards_check.py
|
|
14
|
+
- fork_prompt_reminder.py
|
|
15
|
+
- imitate_figure_reminder.py
|
|
16
|
+
- image_host_reminder.py
|
|
17
|
+
- enrichment_term_check.py
|
|
18
|
+
- humanizer_reminder.py
|
|
19
|
+
- ppt_standard_check.py
|
|
20
|
+
- anti_hallucination_reminder.py
|
|
21
|
+
- answer_format_reminder.py
|
|
22
|
+
- url_format_reminder.py
|
|
23
|
+
- media_link_check.py
|
|
24
|
+
- zotero_first_reminder.py
|
|
25
|
+
- wiki_write_reminder.py
|
|
26
|
+
- memory_route_reminder.py
|
|
27
|
+
- ai_query_reminder.py
|
|
28
|
+
- figure_split_reminder.py
|
|
29
|
+
- test_session_cleanup_reminder.py
|
|
30
|
+
- figure_change_gate_reminder.py
|
|
31
|
+
- secret_store_reminder.py
|
|
32
|
+
- analysis_method_standards_reminder.py
|
|
33
|
+
- figure_text_consistency_reminder.py
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hub-reminders",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./plugin.js",
|
|
6
|
+
"description": "DSH 会话 hook 注入:按白名单运行仓库 hooks/*.py,并把提醒文本注入每一轮(开工自检 / 测试清理 / 实事求是等)",
|
|
7
|
+
"files": [
|
|
8
|
+
"plugin.js",
|
|
9
|
+
"cordis.patch.yml"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/Genesi599/AI-Agent-Hub.git",
|
|
15
|
+
"directory": "dsh/plugins/reminders"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { realpathSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const name = "ai-agent-hub-reminders";
|
|
7
|
+
|
|
8
|
+
const SCRIPT_ALLOWLIST = Object.freeze([
|
|
9
|
+
"session_memory_check.py",
|
|
10
|
+
"report_navigation_reminder.py",
|
|
11
|
+
"hpc_parallel_check.py",
|
|
12
|
+
"memory_update_reminder.py",
|
|
13
|
+
"drawing_standards_check.py",
|
|
14
|
+
"drawing_yh_template_required.py",
|
|
15
|
+
"fork_prompt_reminder.py",
|
|
16
|
+
"imitate_figure_reminder.py",
|
|
17
|
+
"image_host_reminder.py",
|
|
18
|
+
"enrichment_term_check.py",
|
|
19
|
+
"humanizer_reminder.py",
|
|
20
|
+
"ppt_standard_check.py",
|
|
21
|
+
"ppt_from_report_check.py",
|
|
22
|
+
"anti_hallucination_reminder.py",
|
|
23
|
+
"answer_format_reminder.py",
|
|
24
|
+
"url_format_reminder.py",
|
|
25
|
+
"media_link_check.py",
|
|
26
|
+
"analysis_method_standards_reminder.py",
|
|
27
|
+
"figure_text_consistency_reminder.py",
|
|
28
|
+
"zotero_first_reminder.py",
|
|
29
|
+
"wiki_write_reminder.py",
|
|
30
|
+
"memory_route_reminder.py",
|
|
31
|
+
"ai_query_reminder.py",
|
|
32
|
+
"figure_split_reminder.py",
|
|
33
|
+
"test_session_cleanup_reminder.py",
|
|
34
|
+
"figure_change_gate_reminder.py",
|
|
35
|
+
"secret_store_reminder.py",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function textOf(message) {
|
|
39
|
+
return (message?.content ?? [])
|
|
40
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
41
|
+
.map((block) => block.text)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function directPrompt(messages) {
|
|
46
|
+
const direct = [...messages].reverse().find((message) => message?.source?.kind === "user");
|
|
47
|
+
return direct ? textOf(direct) : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Session format v4 only admits producer-owned source kinds: the generic "plugin"
|
|
51
|
+
// wrapper was retired (v3 rows are migrated to `plugin:<name>`). Deep-freeze the
|
|
52
|
+
// message because the durable append requires an immutable value.
|
|
53
|
+
function deepFreeze(value) {
|
|
54
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
55
|
+
Object.freeze(value);
|
|
56
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function noticeMessage(text, summary) {
|
|
62
|
+
return deepFreeze({
|
|
63
|
+
id: randomUUID(),
|
|
64
|
+
role: "user",
|
|
65
|
+
content: [{ type: "text", text }],
|
|
66
|
+
source: {
|
|
67
|
+
kind: `plugin:${name}`,
|
|
68
|
+
form: "notice",
|
|
69
|
+
summary,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function minimalChildEnv() {
|
|
75
|
+
const names = [
|
|
76
|
+
"SystemRoot", "WINDIR", "COMSPEC", "PATH", "PATHEXT",
|
|
77
|
+
"USERPROFILE", "HOME", "HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA",
|
|
78
|
+
"TEMP", "TMP", "LANG", "LC_ALL",
|
|
79
|
+
];
|
|
80
|
+
const env = Object.fromEntries(names.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]));
|
|
81
|
+
// DSH_* 透传(2026-10-29 修复):hooks(如 media_link_check.py)靠 DSH_HOME/DSH_SESSION_ID
|
|
82
|
+
// 分流 DSH/Hermes 发图规则;之前最小白名单把它们剥掉,hook 在 DSH 会话里永远判定为 Hermes,
|
|
83
|
+
// 注入 MEDIA:/#media: 这套 DSH 渲染不了的格式。
|
|
84
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
85
|
+
if (/^DSH_[A-Z0-9_]+$/.test(key)) env[key] = value;
|
|
86
|
+
}
|
|
87
|
+
env.PYTHONIOENCODING = "utf-8";
|
|
88
|
+
env.PYTHONUTF8 = "1";
|
|
89
|
+
return env;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveAllowedScripts(hooksDir, requested = SCRIPT_ALLOWLIST) {
|
|
93
|
+
const root = realpathSync(resolve(hooksDir));
|
|
94
|
+
const allowed = new Set(SCRIPT_ALLOWLIST);
|
|
95
|
+
return requested.map((filename) => {
|
|
96
|
+
if (basename(filename) !== filename || !allowed.has(filename)) {
|
|
97
|
+
throw new Error(`ai-agent-hub-reminders: script is not allowlisted: ${filename}`);
|
|
98
|
+
}
|
|
99
|
+
const script = realpathSync(join(root, filename));
|
|
100
|
+
const rel = relative(root, script);
|
|
101
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
102
|
+
throw new Error(`ai-agent-hub-reminders: script escaped hooksDir: ${filename}`);
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze({ filename, script });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseHookResponse(stdout) {
|
|
109
|
+
const raw = String(stdout ?? "").trim();
|
|
110
|
+
if (!raw) return undefined;
|
|
111
|
+
let data;
|
|
112
|
+
try {
|
|
113
|
+
data = JSON.parse(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const context = data?.hookSpecificOutput?.additionalContext ?? data?.context;
|
|
118
|
+
return typeof context === "string" && context.trim() ? context.trim() : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function runHook(config, script, payload, signal) {
|
|
122
|
+
return new Promise((resolvePromise, reject) => {
|
|
123
|
+
const child = spawn(config.pythonCommand, [script], {
|
|
124
|
+
cwd: config.hooksDir,
|
|
125
|
+
env: minimalChildEnv(),
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
128
|
+
});
|
|
129
|
+
const stdout = [];
|
|
130
|
+
const stderr = [];
|
|
131
|
+
let outputBytes = 0;
|
|
132
|
+
let settled = false;
|
|
133
|
+
const finish = (error, value) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
settled = true;
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
if (signal) signal.removeEventListener("abort", abort);
|
|
138
|
+
error ? reject(error) : resolvePromise(value);
|
|
139
|
+
};
|
|
140
|
+
const abort = () => {
|
|
141
|
+
child.kill();
|
|
142
|
+
finish(new Error(`hook aborted: ${script}`));
|
|
143
|
+
};
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
child.kill();
|
|
146
|
+
finish(new Error(`hook timed out: ${script}`));
|
|
147
|
+
}, config.timeoutMs);
|
|
148
|
+
if (signal) signal.addEventListener("abort", abort, { once: true });
|
|
149
|
+
child.stdout.on("data", (chunk) => {
|
|
150
|
+
outputBytes += chunk.length;
|
|
151
|
+
if (outputBytes > config.maxBufferBytes) {
|
|
152
|
+
child.kill();
|
|
153
|
+
finish(new Error(`hook output exceeded limit: ${script}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
stdout.push(chunk);
|
|
157
|
+
});
|
|
158
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
159
|
+
child.on("error", (error) => finish(error));
|
|
160
|
+
child.on("close", (code) => {
|
|
161
|
+
if (code !== 0) {
|
|
162
|
+
finish(new Error(`hook failed (${code}): ${script}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
finish(undefined, parseHookResponse(Buffer.concat(stdout).toString("utf8")));
|
|
166
|
+
});
|
|
167
|
+
child.stdin.end(payload, "utf8");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function apply(ctx, rawConfig = {}) {
|
|
172
|
+
try {
|
|
173
|
+
return applyInner(ctx, rawConfig)
|
|
174
|
+
} catch (e) {
|
|
175
|
+
try {
|
|
176
|
+
appendFileSync(process.env.TEMP + "/ai-agent-hub-reminders-error.txt",
|
|
177
|
+
new Date().toISOString() + " APPLY-FAIL: " + (e && e.stack || e) + "\n")
|
|
178
|
+
} catch {}
|
|
179
|
+
throw e
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyInner(ctx, rawConfig = {}) {
|
|
184
|
+
const config = {
|
|
185
|
+
hooksDir: rawConfig.hooksDir,
|
|
186
|
+
scripts: rawConfig.scripts ?? SCRIPT_ALLOWLIST,
|
|
187
|
+
pythonCommand: "python",
|
|
188
|
+
timeoutMs: rawConfig.timeoutMs ?? 30000,
|
|
189
|
+
maxBufferBytes: rawConfig.maxBufferBytes ?? 1024 * 1024,
|
|
190
|
+
sessionMemory: rawConfig.sessionMemory !== false,
|
|
191
|
+
};
|
|
192
|
+
if (!config.hooksDir) throw new Error("ai-agent-hub-reminders: hooksDir is required");
|
|
193
|
+
const scripts = resolveAllowedScripts(config.hooksDir, config.scripts);
|
|
194
|
+
const byFilename = new Map(scripts.map((entry) => [entry.filename, entry.script]));
|
|
195
|
+
|
|
196
|
+
const runOne = (filename, eventPayload, signal) => {
|
|
197
|
+
const script = byFilename.get(filename);
|
|
198
|
+
if (!script) return Promise.resolve(undefined);
|
|
199
|
+
return runHook(config, script, JSON.stringify(eventPayload), signal);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// SessionStart-equivalent memory check, injected once per agent on the first model step.
|
|
203
|
+
const sessionMemoryInjected = /* @__PURE__ */ new WeakSet();
|
|
204
|
+
|
|
205
|
+
// UserPromptSubmit channel: inject matching reminders into the next model step.
|
|
206
|
+
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
207
|
+
const decision = await next();
|
|
208
|
+
if (decision.kind === "reject" || signal.aborted) return decision;
|
|
209
|
+
const prompt = directPrompt(messages);
|
|
210
|
+
if (!prompt) return decision;
|
|
211
|
+
|
|
212
|
+
const injections = [];
|
|
213
|
+
const summaries = [];
|
|
214
|
+
if (config.sessionMemory && !sessionMemoryInjected.has(agent) && byFilename.has("session_memory_check.py")) {
|
|
215
|
+
sessionMemoryInjected.add(agent);
|
|
216
|
+
try {
|
|
217
|
+
const context = await runOne("session_memory_check.py", { hook_event_name: "SessionStart" }, signal);
|
|
218
|
+
if (context) {
|
|
219
|
+
injections.push(context);
|
|
220
|
+
summaries.push("session-memory");
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn(`ai-agent-hub-reminders: session-memory failed: ${String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const payload = { hook_event_name: "UserPromptSubmit", prompt, cwd: process.cwd(), _agent_source: "dsh" };
|
|
228
|
+
const settled = await Promise.allSettled(
|
|
229
|
+
scripts
|
|
230
|
+
.filter(({ filename }) => filename !== "session_memory_check.py")
|
|
231
|
+
.map(async ({ filename, script }) => ({ filename, context: await runHook(config, script, JSON.stringify(payload), signal) })),
|
|
232
|
+
);
|
|
233
|
+
for (const result of settled) {
|
|
234
|
+
if (result.status !== "fulfilled" || !result.value.context) continue;
|
|
235
|
+
injections.push(result.value.context);
|
|
236
|
+
summaries.push(result.value.filename.replace(/\.py$/i, ""));
|
|
237
|
+
}
|
|
238
|
+
if (injections.length === 0) return decision;
|
|
239
|
+
return {
|
|
240
|
+
kind: "enter",
|
|
241
|
+
messages: [...decision.messages, noticeMessage(injections.join("\n\n"), summaries.join(", "))],
|
|
242
|
+
};
|
|
243
|
+
}, { prepend: true });
|
|
244
|
+
}
|
package/README.md
DELETED