@stardustlc/dsh-dream 0.3.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stardustlc
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.en.md ADDED
@@ -0,0 +1,56 @@
1
+ [简体中文](README.md)
2
+
3
+ # dsh-dream
4
+
5
+ > **An agent that dreams**: session replay (dream material) → reflection (interpretation) → dream journal (memory consolidation).
6
+
7
+ Humans consolidate memories by replaying the day during sleep — dsh-dream gives DeepSeek Harness agents the same ability. It reads your historical sessions (the official multi-frame zstd session logs, parsed with zero dependencies), distills dream material, lets the agent reflect, and writes permanent dream journal entries that can be recalled later.
8
+
9
+ ## Compatibility
10
+
11
+ Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` (2026-08-31). Follows the cordis patch-bundle model (`cordis.patch.yml` + `dsh.bundle.patch`); no runtime imports of `@deepseek-ai/*` internals. Session reading is layout-blind with respect to the official JSONL-zstd multi-frame format and packed-chunk rows.
12
+
13
+ ## Install / Uninstall
14
+
15
+ ```bash
16
+ dsh plugin --profile web add @stardustlc/dsh-dream
17
+ # or from source:
18
+ dsh plugin --profile web add github:STARDUSTLC666/dsh-dream
19
+ dsh plugin --profile web remove @stardustlc/dsh-dream
20
+ ```
21
+
22
+ Restart the web service afterwards. The dream journal lives at `~/.dsh/.dsh-dream/dreams.jsonl` and survives uninstallation; remove that directory manually for a full cleanup. Part of [dsh-suite](https://github.com/STARDUSTLC666/dsh-suite) — one command installs all 18 STARDUSTLC plugins.
23
+
24
+ ## Tools
25
+
26
+ | Tool | Purpose | Key params |
27
+ | :-- | :-- | :-- |
28
+ | `dream_digest` | Replay recent sessions (title/turns/user quotes/assistant conclusions/tool footprint); subagent sessions skipped; official packed-chunk rows decoded | `maxSessions`; `mode`: full/brief |
29
+ | `dream_save` | Write a reflection + 1-5 lessons + mood to the permanent journal | `reflection` required |
30
+ | `dream_journal` | List dreams, newest first | `limit` |
31
+ | `dream_recall` | Keyword search across the journal | `query` required |
32
+ | `dream_bridge` | Merge top lessons into AGENTS.md (idempotent marker block) — dreams become long-term memory | `path` required |
33
+ | `dream_health` | Self-check: sessions dir / dream count / config summary | — |
34
+
35
+ `dream_bridge` merges the most frequent dream lessons into a target `AGENTS.md` behind idempotent marker blocks; `dream_journal` also reports mood distribution and top lessons.
36
+
37
+ The bundled `dream-protocol` skill teaches the agent when and how to dream (and the journaling discipline: patterns only, never secrets).
38
+
39
+ ## Privacy (on by default)
40
+
41
+ Dream material is masked before journaling: sk keys, GitHub/Groq/Slack tokens, AWS keys, JWTs, `password/token/api_key` assignments and long high-entropy strings become `[masked·type]`. Disable with `maskSecrets: false`.
42
+
43
+ ## Permissions & data
44
+
45
+ Read-only access to the sessions directory; appends JSONL to the journal directory; no network calls. Session content may be sensitive — the protocol forbids secrets in dreams, but the journal is plaintext: review before sharing.
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ pnpm install
51
+ pnpm test # build + 48 tests (multi-frame zstd fixtures, packed-chunk decoding, privacy masking, journal round-trip, bridge idempotency, stats, six tools, registration lifecycle)
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT (see [LICENSE](LICENSE))
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ [English](README.en.md)
2
+
3
+ # dsh-dream
4
+
5
+ > **会做梦的 agent**:会话回放(梦原料)→ 反思(解梦)→ 梦境日记(记忆巩固)。
6
+
7
+ ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-dream) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-dream?style=social)
8
+
9
+ 人睡觉时大脑回放白天的经历、巩固记忆——dsh-dream 让 DeepSeek Harness 的 agent 也拥有这个能力:读取你的历史会话(官方多帧 zstd 会话日志,零依赖解析),提炼梦原料,反思后写入永久梦境日记,下次醒来可以忆梦。
10
+
11
+ ## 兼容性
12
+
13
+ 在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。会话读取兼容官方 JSONL-zstd 多帧格式与 packed-chunk 行(只取所需事件类型,布局无关)。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ dsh plugin --profile web add @stardustlc/dsh-dream
19
+ # 或从源码:
20
+ dsh plugin --profile web add github:STARDUSTLC666/dsh-dream
21
+ ```
22
+
23
+ 安装后重启 Web 服务即可。本插件收录于 [dsh-suite](https://github.com/STARDUSTLC666/dsh-suite) 全家桶——一条命令可装入 STARDUSTLC 全部 18 个插件。
24
+
25
+ ## 卸载
26
+
27
+ ```bash
28
+ dsh plugin --profile web remove @stardustlc/dsh-dream
29
+ ```
30
+
31
+ 卸载后重启 Web 服务。梦境日记默认保存在 `~/.dsh/.dsh-dream/dreams.jsonl`,卸载不会删除;如需彻底清理请手动删除该目录。
32
+
33
+ ## 工具一览
34
+
35
+ | 工具 | 作用 | 关键参数 |
36
+ | :-- | :-- | :-- |
37
+ | `dream_digest` | 入梦:回放最近会话(标题/轮数/用户原话/助手结论/工具足迹),自动跳过子代理,还原官方打包行流式文本 | `maxSessions` 可选;`mode`: full/brief |
38
+ | `dream_save` | 记梦:反思 + 1-5 条教训 + 心境,永久保存 | `reflection` 必填;`lessons`/`mood` 可选 |
39
+ | `dream_journal` | 翻梦:倒序列出历史梦境 | `limit` 可选 |
40
+ | `dream_recall` | 忆梦:关键词检索梦境 | `query` 必填 |
41
+ | `dream_bridge` | 渡梦:把高频教训幂等合并进 AGENTS.md,梦变成长期记忆 | `path` 必填;`maxLessons` 可选 |
42
+ | `dream_health` | 自检:会话目录/梦境计数/配置汇总 | 无 |
43
+
44
+ ### 示例
45
+
46
+ ```text
47
+ dream_digest { maxSessions: 5, mode: 'brief' }
48
+ dream_save { reflection: "用户反复遇到输入法问题,偏好先自查再重启", lessons: ["先问是否重启过应用"], mood: "平静" }
49
+ dream_recall { query: "输入法" }
50
+ dream_bridge { path: "AGENTS.md", maxLessons: 10 }
51
+ ```
52
+
53
+ ## 渡梦:让梦变成长期记忆
54
+
55
+ `dream_bridge` 把梦境日记里出现频次最高的教训合并进目标 `AGENTS.md`:带 `<!-- dsh-dream:lessons:start/end -->` 标记块,重复执行只刷新块内内容(幂等),反复梦到的教训会标注次数。`dream_journal` 同时给出心境分布与最常梦到的教训统计。
56
+
57
+ ## 做梦协议(随包技能)
58
+
59
+ 插件附带 `dream-protocol` 技能,教 agent 何时做梦(开场/收尾/距上次做梦超一天)、做梦三步(入梦→解梦→记梦)与记梦纪律(只沉淀规律不复述流水账、密钥隐私不入梦、存疑要标注)。
60
+
61
+ ## 配置
62
+
63
+ 在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时用默认值也能加载):
64
+
65
+ ```yaml
66
+ - id: dream
67
+ name: '@stardustlc/dsh-dream'
68
+ config:
69
+ # sessionsRoot: '' # 会话根目录(默认 ~/.dsh/sessions)
70
+ # journalDir: '' # 梦境日记目录(默认 ~/.dsh/.dsh-dream)
71
+ maxSessions: 10 # dream_digest 最多回放会话数(1-50)
72
+ maxCharsPerSession: 6000 # 每会话摘要字符上限(500-50000)
73
+ # maxUserMessages: 5 # 每会话保留用户消息条数(1-20)
74
+ # maskSecrets: true # 梦原料隐私脱敏开关(默认开)
75
+ ```
76
+
77
+ ## 隐私保护(默认开启)
78
+
79
+ 梦原料入梦前自动脱敏:sk 密钥、GitHub/Groq/Slack 令牌、AWS 密钥、JWT、`password/token/api_key` 赋值、超长高熵串都会被打成 `[已脱敏·类型]`。不需要时可在配置里设 `maskSecrets: false`。
80
+
81
+ ## 权限与数据
82
+
83
+ - 只读访问会话目录(`~/.dsh/sessions`),不修改任何会话文件;
84
+ - 梦境日记以 JSONL 追加写入 `journalDir`;
85
+ - 不发起任何网络请求;
86
+ - 会话内容可能包含敏感信息——做梦协议明确禁止把密钥/隐私写入梦境,但梦境日记本身是明文存储,请自行评估。
87
+
88
+ ## 排错
89
+
90
+ - `dream_digest` 返回 0 个会话:先运行 `dream_health` 检查会话目录是否存在;确认 DSH 已产生过会话;
91
+ - 梦境写不进去:检查 `journalDir` 可写权限;
92
+ - 加载失败:查看 DSH 启动日志中带 `[dsh-dream]` 前缀的告警。
93
+
94
+ ## 开发
95
+
96
+ ```bash
97
+ pnpm install
98
+ pnpm test # 构建 + 48 个测试(多帧 zstd 夹具、打包行解码、隐私脱敏、梦境日记往返、记忆桥接幂等、统计、六工具、注册生命周期)
99
+ ```
100
+
101
+ ## License
102
+
103
+ MIT(见 [LICENSE](LICENSE))
@@ -0,0 +1,8 @@
1
+ # dsh-dream 组合包补丁:把做梦插件行插入 profile。
2
+ # 用户可在自己 profile 的 cordis.patch.yml 里按 id 覆盖 config(见 README 配置示例)。
3
+ - insert:
4
+ - id: dream
5
+ name: '@stardustlc/dsh-dream'
6
+ config:
7
+ maxSessions: 10
8
+ maxCharsPerSession: 6000
@@ -0,0 +1,15 @@
1
+ export declare const BRIDGE_START = "<!-- dsh-dream:lessons:start\uFF08\u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u52FF\u624B\u5DE5\u7F16\u8F91\u5757\u5185\u5185\u5BB9\uFF09 -->";
2
+ export declare const BRIDGE_END = "<!-- dsh-dream:lessons:end -->";
3
+ /** 生成教训块文本。 */
4
+ export declare function buildLessonsBlock(lessons: Array<{
5
+ lesson: string;
6
+ count: number;
7
+ }>): string;
8
+ /**
9
+ * 把梦境教训合并进目标文件:无文件则创建,有标记块则替换,无标记块则追加。
10
+ * @returns 动作类型与写入的教训数。
11
+ */
12
+ export declare function bridgeDreams(journalDir: string, targetPath: string, maxLessons: number): {
13
+ action: 'created' | 'replaced' | 'appended';
14
+ lessonsCount: number;
15
+ };
package/lib/bridge.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 记忆桥接:把梦境里的高频教训合并进 AGENTS.md(幂等,带标记块)。
3
+ *
4
+ * @module dsh-dream/bridge
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import { dreamStats } from './journal.js';
9
+ export const BRIDGE_START = '<!-- dsh-dream:lessons:start(自动生成,请勿手工编辑块内内容) -->';
10
+ export const BRIDGE_END = '<!-- dsh-dream:lessons:end -->';
11
+ /** 生成教训块文本。 */
12
+ export function buildLessonsBlock(lessons) {
13
+ const lines = [BRIDGE_START, '', '## 梦境沉淀(dsh-dream 自动生成)', ''];
14
+ for (const item of lessons) {
15
+ lines.push('- ' + item.lesson + (item.count > 1 ? '(反复梦到 ' + item.count + ' 次)' : ''));
16
+ }
17
+ lines.push('', BRIDGE_END);
18
+ return lines.join('\n');
19
+ }
20
+ /**
21
+ * 把梦境教训合并进目标文件:无文件则创建,有标记块则替换,无标记块则追加。
22
+ * @returns 动作类型与写入的教训数。
23
+ */
24
+ export function bridgeDreams(journalDir, targetPath, maxLessons) {
25
+ const stats = dreamStats(journalDir);
26
+ if (stats.total === 0) {
27
+ throw new Error('还没有做过梦:请先用 dream_digest + dream_save 做梦,再来桥接记忆。');
28
+ }
29
+ const lessons = stats.topLessons.slice(0, maxLessons);
30
+ const block = buildLessonsBlock(lessons);
31
+ if (!existsSync(targetPath)) {
32
+ mkdirSync(dirname(targetPath), { recursive: true });
33
+ writeFileSync(targetPath, block + '\n', 'utf8');
34
+ return { action: 'created', lessonsCount: lessons.length };
35
+ }
36
+ const current = readFileSync(targetPath, 'utf8');
37
+ const startIdx = current.indexOf(BRIDGE_START);
38
+ const endIdx = current.indexOf(BRIDGE_END);
39
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
40
+ const next = current.slice(0, startIdx) + block + current.slice(endIdx + BRIDGE_END.length);
41
+ writeFileSync(targetPath, next, 'utf8');
42
+ return { action: 'replaced', lessonsCount: lessons.length };
43
+ }
44
+ writeFileSync(targetPath, current.replace(/\s*$/, '') + '\n\n' + block + '\n', 'utf8');
45
+ return { action: 'appended', lessonsCount: lessons.length };
46
+ }
@@ -0,0 +1,17 @@
1
+ /** 已解析配置。 */
2
+ export interface ResolvedDreamConfig {
3
+ /** 会话根目录(默认 ~/.dsh/sessions)。 */
4
+ sessionsRoot: string;
5
+ /** 梦境日记目录(默认 ~/.dsh/.dsh-dream)。 */
6
+ journalDir: string;
7
+ /** dream_digest 最多回放的会话数(1-50,默认 10)。 */
8
+ maxSessions: number;
9
+ /** 每个会话摘要的最大字符数(500-50000,默认 6000)。 */
10
+ maxCharsPerSession: number;
11
+ /** 每个会话最多保留的用户消息条数(1-20,默认 5)。 */
12
+ maxUserMessages: number;
13
+ /** 梦原料入梦前是否做隐私脱敏(默认 true)。 */
14
+ maskSecrets: boolean;
15
+ }
16
+ /** 解析并校验插件配置。 */
17
+ export declare function resolveConfig(raw?: Record<string, unknown> | null): ResolvedDreamConfig;
package/lib/config.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-dream 配置解析:会话根目录、梦境日记目录与摘要上限。
3
+ *
4
+ * @module dsh-dream/config
5
+ */
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ function str(value, fallback) {
9
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : fallback;
10
+ }
11
+ function clamp(value, fallback, min, max) {
12
+ if (typeof value !== 'number' || !Number.isFinite(value))
13
+ return fallback;
14
+ return Math.min(max, Math.max(min, Math.round(value)));
15
+ }
16
+ /** 解析并校验插件配置。 */
17
+ export function resolveConfig(raw) {
18
+ const cfg = raw ?? {};
19
+ const home = homedir();
20
+ return {
21
+ sessionsRoot: str(cfg.sessionsRoot, join(home, '.dsh', 'sessions')),
22
+ journalDir: str(cfg.journalDir, join(home, '.dsh', '.dsh-dream')),
23
+ maxSessions: clamp(cfg.maxSessions, 10, 1, 50),
24
+ maxCharsPerSession: clamp(cfg.maxCharsPerSession, 6000, 500, 50000),
25
+ maxUserMessages: clamp(cfg.maxUserMessages, 5, 1, 20),
26
+ maskSecrets: cfg.maskSecrets !== false,
27
+ };
28
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type DreamToolDefinition } from './tools.js';
2
+ /** cordis 服务注入:要用 ctx.tools 与 ctx.skills。 */
3
+ export declare const name = "dream";
4
+ export declare const inject: string[];
5
+ /** 插件所需的最小 ctx 面。 */
6
+ export interface DreamPluginContext {
7
+ tools: {
8
+ register(definition: DreamToolDefinition): () => void;
9
+ };
10
+ skills?: {
11
+ register(definition: {
12
+ name: string;
13
+ description: string;
14
+ content: string;
15
+ resourceBase: {
16
+ kind: 'directory';
17
+ path: string;
18
+ };
19
+ source?: string;
20
+ }): () => void;
21
+ };
22
+ on?(event: string, listener: () => void): () => void;
23
+ }
24
+ /** 随包技能目录。 */
25
+ export declare function bundledSkillsDir(): string;
26
+ /** 插件入口。 */
27
+ export declare function apply(ctx: DreamPluginContext, config?: Record<string, unknown> | null): void;
28
+ export * from './config.js';
29
+ export * from './sessions.js';
30
+ export * from './journal.js';
31
+ export * from './tools.js';
32
+ export * from './mask.js';
33
+ export * from './bridge.js';
package/lib/index.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * dsh-dream —— 做梦插件:会话回放(梦原料)→ 反思 → 梦境日记(记忆巩固)。
3
+ *
4
+ * @module dsh-dream
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { join } from 'node:path';
9
+ import { resolveConfig } from './config.js';
10
+ import { buildDreamTools } from './tools.js';
11
+ /** cordis 服务注入:要用 ctx.tools 与 ctx.skills。 */
12
+ export const name = 'dream';
13
+ export const inject = ['tools', 'skills'];
14
+ /** 随包技能目录。 */
15
+ export function bundledSkillsDir() {
16
+ return fileURLToPath(new URL('../skills/', import.meta.url));
17
+ }
18
+ /** 插件入口。 */
19
+ export function apply(ctx, config) {
20
+ let cfg;
21
+ try {
22
+ cfg = resolveConfig(config);
23
+ }
24
+ catch (error) {
25
+ console.warn('[dsh-dream] ' + (error instanceof Error ? error.message : String(error)));
26
+ cfg = resolveConfig(null);
27
+ }
28
+ const disposers = [];
29
+ for (const definition of buildDreamTools(cfg)) {
30
+ disposers.push(ctx.tools.register(definition));
31
+ }
32
+ // 注册做梦协议技能(文件缺失只告警,不阻断)
33
+ if (typeof ctx.skills?.register === 'function') {
34
+ try {
35
+ const dir = join(bundledSkillsDir(), 'dream-protocol');
36
+ const text = readFileSync(join(dir, 'SKILL.md'), 'utf8');
37
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
38
+ let description = '做梦协议:会话回放、反思与梦境日记。';
39
+ if (match !== null) {
40
+ const descMatch = /description:\s*(.+)/.exec(match[1]);
41
+ if (descMatch !== null)
42
+ description = descMatch[1].trim();
43
+ }
44
+ disposers.push(ctx.skills.register({
45
+ name: 'dream-protocol',
46
+ description,
47
+ content: match !== null ? text.slice(match[0].length).trimStart() : text,
48
+ resourceBase: { kind: 'directory', path: dir },
49
+ source: 'runtime',
50
+ }));
51
+ }
52
+ catch (error) {
53
+ console.warn('[dsh-dream] 做梦协议技能加载失败:' + (error instanceof Error ? error.message : String(error)));
54
+ }
55
+ }
56
+ if (typeof ctx.on === 'function') {
57
+ ctx.on('dispose', () => {
58
+ for (const dispose of disposers)
59
+ dispose();
60
+ });
61
+ }
62
+ }
63
+ export * from './config.js';
64
+ export * from './sessions.js';
65
+ export * from './journal.js';
66
+ export * from './tools.js';
67
+ export * from './mask.js';
68
+ export * from './bridge.js';
@@ -0,0 +1,26 @@
1
+ /** 一条梦境。 */
2
+ export interface DreamEntry {
3
+ id: string;
4
+ at: string;
5
+ reflection: string;
6
+ lessons: string[];
7
+ mood: string;
8
+ }
9
+ export declare function journalFile(journalDir: string): string;
10
+ /** 追加一条梦;返回落盘后的条目。 */
11
+ export declare function saveDream(journalDir: string, reflection: string, lessons: string[], mood: string): DreamEntry;
12
+ /** 倒序读取梦境(新梦在前);损坏行跳过。 */
13
+ export declare function readDreams(journalDir: string, limit: number): DreamEntry[];
14
+ /** 梦境统计:总数、心境分布、高频教训。 */
15
+ export interface DreamStats {
16
+ total: number;
17
+ moods: Record<string, number>;
18
+ topLessons: Array<{
19
+ lesson: string;
20
+ count: number;
21
+ }>;
22
+ }
23
+ /** 统计梦境(基于全量日记)。 */
24
+ export declare function dreamStats(journalDir: string): DreamStats;
25
+ /** 关键词检索梦境(不区分大小写,命中 reflection/lessons)。 */
26
+ export declare function searchDreams(journalDir: string, query: string, limit: number): DreamEntry[];
package/lib/journal.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 梦境日记:JSONL 追加式存储(每行一条梦),倒序读取与关键词检索。
3
+ *
4
+ * @module dsh-dream/journal
5
+ */
6
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ export function journalFile(journalDir) {
9
+ return join(journalDir, 'dreams.jsonl');
10
+ }
11
+ /** 追加一条梦;返回落盘后的条目。 */
12
+ export function saveDream(journalDir, reflection, lessons, mood) {
13
+ const trimmed = reflection.trim();
14
+ if (trimmed === '')
15
+ throw new Error('梦境不能为空:请在 reflection 里写下你的反思。');
16
+ const entry = {
17
+ id: 'dream-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8),
18
+ at: new Date().toISOString(),
19
+ reflection: trimmed,
20
+ lessons: lessons.map((lesson) => lesson.trim()).filter((lesson) => lesson !== ''),
21
+ mood: mood.trim() || '平静',
22
+ };
23
+ mkdirSync(journalDir, { recursive: true });
24
+ appendFileSync(journalFile(journalDir), JSON.stringify(entry) + '\n', 'utf8');
25
+ return entry;
26
+ }
27
+ /** 倒序读取梦境(新梦在前);损坏行跳过。 */
28
+ export function readDreams(journalDir, limit) {
29
+ const file = journalFile(journalDir);
30
+ if (!existsSync(file))
31
+ return [];
32
+ const lines = readFileSync(file, 'utf8').split('\n').filter((line) => line.trim() !== '');
33
+ const out = [];
34
+ for (const line of lines) {
35
+ try {
36
+ const rec = JSON.parse(line);
37
+ if (typeof rec.reflection === 'string') {
38
+ out.push({
39
+ id: String(rec.id ?? ''),
40
+ at: String(rec.at ?? ''),
41
+ reflection: rec.reflection,
42
+ lessons: Array.isArray(rec.lessons) ? rec.lessons.filter((l) => typeof l === 'string') : [],
43
+ mood: typeof rec.mood === 'string' ? rec.mood : '',
44
+ });
45
+ }
46
+ }
47
+ catch { /* 损坏行跳过 */ }
48
+ }
49
+ return out.reverse().slice(0, limit);
50
+ }
51
+ /** 统计梦境(基于全量日记)。 */
52
+ export function dreamStats(journalDir) {
53
+ const dreams = readDreams(journalDir, 100000);
54
+ const moods = {};
55
+ const lessonCount = new Map();
56
+ for (const dream of dreams) {
57
+ const mood = dream.mood !== '' ? dream.mood : '平静';
58
+ moods[mood] = (moods[mood] ?? 0) + 1;
59
+ for (const lesson of dream.lessons) {
60
+ const key = lesson.toLowerCase();
61
+ lessonCount.set(key, (lessonCount.get(key) ?? 0) + 1);
62
+ }
63
+ }
64
+ const topLessons = [...lessonCount.entries()]
65
+ .sort((a, b) => b[1] - a[1])
66
+ .slice(0, 10)
67
+ .map(([key, count]) => {
68
+ const original = dreams.flatMap((d) => d.lessons).find((l) => l.toLowerCase() === key) ?? key;
69
+ return { lesson: original, count };
70
+ });
71
+ return { total: dreams.length, moods, topLessons };
72
+ }
73
+ /** 关键词检索梦境(不区分大小写,命中 reflection/lessons)。 */
74
+ export function searchDreams(journalDir, query, limit) {
75
+ const needle = query.toLowerCase();
76
+ return readDreams(journalDir, 1000).filter((entry) => {
77
+ const haystack = (entry.reflection + ' ' + entry.lessons.join(' ')).toLowerCase();
78
+ return haystack.includes(needle);
79
+ }).slice(0, limit);
80
+ }
package/lib/mask.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 隐私脱敏:梦原料入梦前对密钥/令牌/凭据打码。
3
+ * 只处理明显模式与超长高熵串,宁可漏报不误伤正常文本。
4
+ *
5
+ * @module dsh-dream/mask
6
+ */
7
+ /** 已知密钥/令牌模式。 */
8
+ export declare const SECRET_PATTERNS: Array<{
9
+ re: RegExp;
10
+ label: string;
11
+ }>;
12
+ /** 对文本做脱敏;无命中时原样返回。 */
13
+ export declare function maskSecrets(text: string): string;
package/lib/mask.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 隐私脱敏:梦原料入梦前对密钥/令牌/凭据打码。
3
+ * 只处理明显模式与超长高熵串,宁可漏报不误伤正常文本。
4
+ *
5
+ * @module dsh-dream/mask
6
+ */
7
+ /** 已知密钥/令牌模式。 */
8
+ export const SECRET_PATTERNS = [
9
+ { re: /sk-[A-Za-z0-9_-]{16,}/g, label: '疑似 sk 密钥' },
10
+ { re: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}/g, label: '疑似 GitHub 令牌' },
11
+ { re: /github_pat_[A-Za-z0-9_]{16,}/g, label: '疑似 GitHub 细粒度令牌' },
12
+ { re: /gsk_[A-Za-z0-9]{16,}/g, label: '疑似 Groq 密钥' },
13
+ { re: /xox[baprs]-[A-Za-z0-9-]{10,}/g, label: '疑似 Slack 令牌' },
14
+ { re: /AKIA[0-9A-Z]{16}/g, label: '疑似 AWS 访问密钥' },
15
+ { re: /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/g, label: '疑似 JWT' },
16
+ ];
17
+ /** 凭据赋值模式:password/secret/token/apikey = 值。 */
18
+ const CRED_ASSIGN = /(password|passwd|secret|token|api[_-]?key)(\s*[:=:]\s*)([^\s"',,。]{6,})/gi;
19
+ /** 超长高熵串:40+ 位字母数字混合(含大小写或数字)。 */
20
+ const LONG_TOKEN = /(?<![A-Za-z0-9])[A-Za-z0-9_\-+/=]{40,}(?![A-Za-z0-9])/g;
21
+ function looksHighEntropy(text) {
22
+ return /[a-z]/.test(text) && (/[A-Z]/.test(text) || /[0-9]/.test(text));
23
+ }
24
+ /** 对文本做脱敏;无命中时原样返回。 */
25
+ export function maskSecrets(text) {
26
+ let out = text;
27
+ for (const { re, label } of SECRET_PATTERNS) {
28
+ out = out.replace(re, '[已脱敏·' + label + ']');
29
+ }
30
+ out = out.replace(CRED_ASSIGN, (_m, key, sep) => key + sep + '[已脱敏·凭据]');
31
+ out = out.replace(LONG_TOKEN, (m) => (looksHighEntropy(m) ? '[已脱敏·长令牌]' : m));
32
+ return out;
33
+ }
@@ -0,0 +1,38 @@
1
+ /** zstd 帧魔数。 */
2
+ export declare const ZSTD_MAGIC: readonly [40, 181, 47, 253];
3
+ /** 会话头。 */
4
+ export interface SessionHeader {
5
+ id: string;
6
+ createdAt: number;
7
+ cwd: string;
8
+ origin: string;
9
+ delegationDepth: number;
10
+ agentPreset: string;
11
+ }
12
+ /** 会话摘要(梦的原料)。 */
13
+ export interface SessionDigest {
14
+ id: string;
15
+ createdAt: number;
16
+ cwd: string;
17
+ agentPreset: string;
18
+ origin: string;
19
+ title: string;
20
+ turns: number;
21
+ userMessages: string[];
22
+ assistantTail: string[];
23
+ toolCalls: string[];
24
+ /** 流式文本还原尾部(来自 text-chunks 打包行,assistant/message 缺失时的兜底梦原料)。 */
25
+ streamTail: string;
26
+ endedAt: number | null;
27
+ }
28
+ /** 扫描 zstd 帧边界(按魔数;误命中由解压失败兜底)。 */
29
+ export declare function frameRanges(buf: Uint8Array): Array<{
30
+ start: number;
31
+ end: number;
32
+ }>;
33
+ /** 多帧解压为文本(损坏帧跳过)。 */
34
+ export declare function decompressAll(buf: Uint8Array): string;
35
+ /** 解析单个会话文件为摘要;文件不存在或损坏时返回 null。 */
36
+ export declare function digestSessionFile(filePath: string, maxUserMessages: number): SessionDigest | null;
37
+ /** 列出指定根目录下最近修改的会话文件路径(跳过子代理会话目录由调用方按摘要 origin 过滤)。 */
38
+ export declare function listSessionFiles(sessionsRoot: string, limit: number): string[];
@@ -0,0 +1,197 @@
1
+ /**
2
+ * DSH 会话日志读取:官方格式为“多帧 zstd 拼接的 JSONL”。
3
+ * 首个逻辑行是会话头({ type: 'session', ... }),其后每行一条存储记录({ type, seq, time, data })。
4
+ * 本模块只读不写;损坏帧/行一律容错跳过。
5
+ *
6
+ * @module dsh-dream/sessions
7
+ */
8
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { zstdDecompressSync } from 'node:zlib';
11
+ /** zstd 帧魔数。 */
12
+ export const ZSTD_MAGIC = [0x28, 0xb5, 0x2f, 0xfd];
13
+ /** 扫描 zstd 帧边界(按魔数;误命中由解压失败兜底)。 */
14
+ export function frameRanges(buf) {
15
+ const starts = [];
16
+ for (let i = 0; i + 3 < buf.length; i++) {
17
+ if (buf[i] === ZSTD_MAGIC[0] && buf[i + 1] === ZSTD_MAGIC[1] && buf[i + 2] === ZSTD_MAGIC[2] && buf[i + 3] === ZSTD_MAGIC[3]) {
18
+ starts.push(i);
19
+ }
20
+ }
21
+ return starts.map((start, i) => ({ start, end: i + 1 < starts.length ? starts[i + 1] : buf.length }));
22
+ }
23
+ /** 多帧解压为文本(损坏帧跳过)。 */
24
+ export function decompressAll(buf) {
25
+ const parts = [];
26
+ for (const { start, end } of frameRanges(buf)) {
27
+ try {
28
+ parts.push(zstdDecompressSync(buf.subarray(start, end)));
29
+ }
30
+ catch { /* 误命中魔数的噪声区段,跳过 */ }
31
+ }
32
+ return Buffer.concat(parts).toString('utf8');
33
+ }
34
+ /** 提取任意载荷的纯文本:兼容字符串与官方 ContentBlock 数组({type:'text', text})。 */
35
+ function blockText(value) {
36
+ if (typeof value === 'string')
37
+ return value;
38
+ if (Array.isArray(value)) {
39
+ return value
40
+ .map((item) => {
41
+ if (typeof item === 'string')
42
+ return item;
43
+ if (typeof item === 'object' && item !== null) {
44
+ const rec = item;
45
+ return typeof rec.text === 'string' ? rec.text : '';
46
+ }
47
+ return '';
48
+ })
49
+ .filter((text) => text !== '')
50
+ .join('');
51
+ }
52
+ if (typeof value === 'object' && value !== null) {
53
+ const rec = value;
54
+ for (const key of ['text', 'content']) {
55
+ const inner = blockText(rec[key]);
56
+ if (inner !== '')
57
+ return inner;
58
+ }
59
+ }
60
+ return '';
61
+ }
62
+ function textOf(data) {
63
+ if (typeof data === 'string')
64
+ return data;
65
+ if (typeof data === 'object' && data !== null) {
66
+ const rec = data;
67
+ for (const key of ['text', 'content', 'message', 'title']) {
68
+ const value = blockText(rec[key]);
69
+ if (value !== '')
70
+ return value;
71
+ }
72
+ }
73
+ return '';
74
+ }
75
+ /** 解析单个会话文件为摘要;文件不存在或损坏时返回 null。 */
76
+ export function digestSessionFile(filePath, maxUserMessages) {
77
+ if (!existsSync(filePath))
78
+ return null;
79
+ let text;
80
+ try {
81
+ text = decompressAll(readFileSync(filePath));
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ const lines = text.split('\n').filter((line) => line.trim() !== '');
87
+ if (lines.length === 0)
88
+ return null;
89
+ let header = null;
90
+ try {
91
+ const first = JSON.parse(lines[0]);
92
+ if (first.type !== 'session')
93
+ return null;
94
+ header = {
95
+ id: String(first.id ?? ''),
96
+ createdAt: typeof first.createdAt === 'number' ? first.createdAt : 0,
97
+ cwd: String(first.cwd ?? ''),
98
+ origin: String(first.origin ?? ''),
99
+ delegationDepth: typeof first.delegationDepth === 'number' ? first.delegationDepth : 0,
100
+ agentPreset: String(first.agentPreset ?? ''),
101
+ };
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ const digest = {
107
+ ...header,
108
+ title: '',
109
+ turns: 0,
110
+ userMessages: [],
111
+ assistantTail: [],
112
+ toolCalls: [],
113
+ streamTail: '',
114
+ endedAt: null,
115
+ };
116
+ for (const line of lines.slice(1)) {
117
+ let rec;
118
+ try {
119
+ rec = JSON.parse(line);
120
+ }
121
+ catch {
122
+ continue;
123
+ }
124
+ const type = String(rec.type ?? '');
125
+ const data = rec.data;
126
+ const time = typeof rec.time === 'number' ? rec.time : null;
127
+ if (time !== null && (digest.endedAt === null || time > digest.endedAt))
128
+ digest.endedAt = time;
129
+ if (type === 'session/title') {
130
+ digest.title = textOf(data);
131
+ }
132
+ else if (type === 'turn/start') {
133
+ digest.turns++;
134
+ }
135
+ else if (type === 'user/message') {
136
+ const msg = textOf(data);
137
+ if (msg !== '' && digest.userMessages.length < maxUserMessages * 2)
138
+ digest.userMessages.push(msg);
139
+ }
140
+ else if (type === 'assistant/message') {
141
+ const msg = textOf(data);
142
+ if (msg !== '') {
143
+ digest.assistantTail.push(msg);
144
+ if (digest.assistantTail.length > 3)
145
+ digest.assistantTail.shift();
146
+ }
147
+ }
148
+ else if (type === 'tool/call') {
149
+ const name = typeof data === 'object' && data !== null ? String(data.name ?? '') : '';
150
+ if (name !== '' && digest.toolCalls.length < 200)
151
+ digest.toolCalls.push(name);
152
+ }
153
+ else if (type === 'text-chunks') {
154
+ // 官方打包行:{ data: { texts: string[], ... } },还原流式正文
155
+ const texts = typeof data === 'object' && data !== null ? data.texts : undefined;
156
+ if (Array.isArray(texts)) {
157
+ digest.streamTail += texts.filter((t) => typeof t === 'string').join('');
158
+ if (digest.streamTail.length > 4000)
159
+ digest.streamTail = digest.streamTail.slice(-4000);
160
+ }
161
+ }
162
+ else if (type === 'tool-call-chunks') {
163
+ // 官方打包行:还原工具名足迹
164
+ const name = typeof data === 'object' && data !== null ? String(data.name ?? '') : '';
165
+ if (name !== '' && digest.toolCalls.length < 200)
166
+ digest.toolCalls.push(name);
167
+ }
168
+ }
169
+ digest.userMessages = digest.userMessages.slice(0, maxUserMessages);
170
+ return digest;
171
+ }
172
+ /** 列出指定根目录下最近修改的会话文件路径(跳过子代理会话目录由调用方按摘要 origin 过滤)。 */
173
+ export function listSessionFiles(sessionsRoot, limit) {
174
+ if (!existsSync(sessionsRoot))
175
+ return [];
176
+ const out = [];
177
+ for (const project of readdirSync(sessionsRoot)) {
178
+ const projectDir = join(sessionsRoot, project);
179
+ try {
180
+ if (!statSync(projectDir).isDirectory())
181
+ continue;
182
+ }
183
+ catch {
184
+ continue;
185
+ }
186
+ for (const session of readdirSync(projectDir)) {
187
+ const file = join(projectDir, session, 'session.jsonl.zstd');
188
+ try {
189
+ if (existsSync(file))
190
+ out.push({ file, mtime: statSync(file).mtimeMs });
191
+ }
192
+ catch { /* 并发删除等竞态,跳过 */ }
193
+ }
194
+ }
195
+ out.sort((a, b) => b.mtime - a.mtime);
196
+ return out.slice(0, limit).map((item) => item.file);
197
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type ResolvedDreamConfig } from './config.js';
2
+ import { type SessionDigest } from './sessions.js';
3
+ /** 模型可见的内容块。 */
4
+ export interface ContentBlock {
5
+ type: 'text';
6
+ text: string;
7
+ }
8
+ /** 注册给 ctx.tools.register 的原始工具定义。 */
9
+ export interface DreamToolDefinition {
10
+ name: string;
11
+ description: string;
12
+ parameters: {
13
+ type: 'object';
14
+ properties: Record<string, unknown>;
15
+ required?: string[];
16
+ };
17
+ output: {
18
+ schema: Record<string, unknown>;
19
+ render(args: unknown, value: unknown): ContentBlock[];
20
+ };
21
+ execute(args: unknown, exec: unknown): Promise<unknown>;
22
+ timeoutMs?: number;
23
+ }
24
+ /** 构建六个做梦工具。 */
25
+ export declare function buildDreamTools(config: ResolvedDreamConfig): DreamToolDefinition[];
26
+ /** 把会话摘要拼成一段可读文本(供模型一次性阅读)。 */
27
+ export declare function buildDigestText(digest: SessionDigest): string;
package/lib/tools.js ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * 五个面向模型的做梦工具:dream_digest / dream_save / dream_journal / dream_recall / dream_health。
3
+ *
4
+ * @module dsh-dream/tools
5
+ */
6
+ import { existsSync } from 'node:fs';
7
+ import { maskSecrets } from './mask.js';
8
+ import { dreamStats, readDreams, saveDream, searchDreams } from './journal.js';
9
+ import { bridgeDreams } from './bridge.js';
10
+ import { digestSessionFile, listSessionFiles } from './sessions.js';
11
+ function compileParameters(spec) {
12
+ const properties = {};
13
+ const required = [];
14
+ for (const [key, prop] of Object.entries(spec)) {
15
+ if (prop?.required === true)
16
+ required.push(key);
17
+ const node = {};
18
+ if (typeof prop?.type === 'string')
19
+ node.type = prop.type;
20
+ if (typeof prop?.description === 'string')
21
+ node.description = prop.description;
22
+ if (prop?.type === 'array' && typeof prop.items === 'object')
23
+ node.items = { type: 'string' };
24
+ properties[key] = node;
25
+ }
26
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
27
+ }
28
+ function asRecord(value) {
29
+ return typeof value === 'object' && value !== null ? value : {};
30
+ }
31
+ function optionalString(args, key) {
32
+ const value = args[key];
33
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
34
+ }
35
+ function requiredString(args, key, label) {
36
+ const value = optionalString(args, key);
37
+ if (value === undefined)
38
+ throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
39
+ return value;
40
+ }
41
+ function stringArray(args, key) {
42
+ const value = args[key];
43
+ if (!Array.isArray(value))
44
+ return [];
45
+ return value.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim());
46
+ }
47
+ function clip(text, max) {
48
+ return text.length > max ? text.slice(0, max) + '…(已截断)' : text;
49
+ }
50
+ const baseSchema = { type: 'object', additionalProperties: true };
51
+ /** 构建六个做梦工具。 */
52
+ export function buildDreamTools(config) {
53
+ const cfg = config;
54
+ const dreamDigest = {
55
+ name: 'dream_digest',
56
+ description: '入梦:回放最近会话的梦原料。读取 DSH 会话日志(多帧 zstd),返回最近 N 个会话的标题、轮数、用户原话摘录、助手结论尾段与工具足迹;自动跳过子代理会话。拿到摘要后请反思并用 dream_save 记梦。',
57
+ parameters: compileParameters({
58
+ maxSessions: { type: 'integer', description: '回放会话数(可选,默认配置值,上限 50)。' },
59
+ mode: { type: 'string', description: '摘要模式:full(默认,全量原料)/ brief(仅意图与结论,省 token)。' },
60
+ }),
61
+ output: {
62
+ schema: baseSchema,
63
+ render: (_args, value) => {
64
+ const rec = asRecord(value);
65
+ const sessions = Array.isArray(rec.sessions) ? rec.sessions : [];
66
+ const lines = ['梦原料:最近 ' + sessions.length + ' 个会话'];
67
+ for (const item of sessions) {
68
+ const s = asRecord(item);
69
+ lines.push('- ' + (s.title !== '' ? s.title : '(无标题)') + ':' + s.turns + ' 轮,目录 ' + s.cwd);
70
+ }
71
+ return [{ type: 'text', text: lines.join('\n') }];
72
+ },
73
+ },
74
+ async execute(rawArgs) {
75
+ const args = asRecord(rawArgs);
76
+ const maxRaw = args.maxSessions;
77
+ const max = typeof maxRaw === 'number' && Number.isInteger(maxRaw) ? Math.min(50, Math.max(1, maxRaw)) : cfg.maxSessions;
78
+ const mode = optionalString(args, 'mode') === 'brief' ? 'brief' : 'full';
79
+ const mask = (text) => cfg.maskSecrets ? maskSecrets(text) : text;
80
+ const files = listSessionFiles(cfg.sessionsRoot, max * 3);
81
+ const sessions = [];
82
+ for (const file of files) {
83
+ if (sessions.length >= max)
84
+ break;
85
+ const digest = digestSessionFile(file, cfg.maxUserMessages);
86
+ if (digest === null)
87
+ continue;
88
+ if (digest.origin === 'subagent')
89
+ continue;
90
+ sessions.push({
91
+ id: digest.id,
92
+ title: digest.title,
93
+ createdAt: digest.createdAt,
94
+ endedAt: digest.endedAt,
95
+ cwd: digest.cwd,
96
+ turns: digest.turns,
97
+ agentPreset: digest.agentPreset,
98
+ userMessages: digest.userMessages.map((msg) => clip(mask(msg), 400)),
99
+ assistantTail: digest.assistantTail.map((msg) => clip(mask(msg), 600)),
100
+ toolCalls: [...new Set(digest.toolCalls)],
101
+ digestText: clip(mask(buildDigestText(digest)), mode === 'brief' ? 400 : cfg.maxCharsPerSession),
102
+ });
103
+ }
104
+ return { count: sessions.length, sessions, mode };
105
+ },
106
+ timeoutMs: 120000,
107
+ };
108
+ const dreamSave = {
109
+ name: 'dream_save',
110
+ description: '记梦:把反思写入梦境日记(永久保存)。reflection 用第一人称写感悟;lessons 列 1-5 条以动词开头的可执行教训;mood 是本次梦的心境(如 平静/兴奋/存疑)。严禁写入密钥与隐私。',
111
+ parameters: compileParameters({
112
+ reflection: { type: 'string', required: true, description: '梦的反思正文(必填,第一人称)。' },
113
+ lessons: { type: 'array', items: { type: 'string' }, description: '1-5 条教训(可选,每条以动词开头)。' },
114
+ mood: { type: 'string', description: '心境(可选,默认 平静)。' },
115
+ }),
116
+ output: {
117
+ schema: baseSchema,
118
+ render: (_args, value) => {
119
+ const rec = asRecord(value);
120
+ return [{ type: 'text', text: '梦已记下(' + rec.id + ',' + rec.at + '),愿醒来时更聪明。' }];
121
+ },
122
+ },
123
+ async execute(rawArgs) {
124
+ const args = asRecord(rawArgs);
125
+ const reflection = requiredString(args, 'reflection', '梦境反思');
126
+ const lessons = stringArray(args, 'lessons').slice(0, 5);
127
+ const mood = optionalString(args, 'mood') ?? '';
128
+ const entry = saveDream(cfg.journalDir, reflection, lessons, mood);
129
+ return { ok: true, ...entry };
130
+ },
131
+ timeoutMs: 15000,
132
+ };
133
+ const dreamJournal = {
134
+ name: 'dream_journal',
135
+ description: '翻梦:倒序列出历史梦境日记(新梦在前)。做梦前先翻翻,避免重复做同一个梦。',
136
+ parameters: compileParameters({
137
+ limit: { type: 'integer', description: '条数上限 1-50(默认 10)。' },
138
+ }),
139
+ output: {
140
+ schema: baseSchema,
141
+ render: (_args, value) => {
142
+ const rec = asRecord(value);
143
+ const dreams = Array.isArray(rec.dreams) ? rec.dreams : [];
144
+ const lines = ['梦境日记共 ' + dreams.length + ' 条:'];
145
+ for (const item of dreams) {
146
+ const d = asRecord(item);
147
+ lines.push('- [' + d.at + '](' + d.mood + ')' + String(d.reflection ?? '').slice(0, 80));
148
+ }
149
+ const stats = asRecord(rec.stats);
150
+ if (typeof stats.total === 'number' && stats.total > 0) {
151
+ const moods = stats.moods;
152
+ if (moods !== undefined) {
153
+ lines.push('· 心境分布:' + Object.entries(moods).map(([mood, n]) => mood + '×' + n).join(','));
154
+ }
155
+ const top = Array.isArray(stats.topLessons) ? stats.topLessons : [];
156
+ if (top.length > 0) {
157
+ const first = asRecord(top[0]);
158
+ lines.push('· 最常梦到的教训:' + first.lesson + '(' + first.count + ' 次)');
159
+ }
160
+ }
161
+ return [{ type: 'text', text: lines.join('\n') }];
162
+ },
163
+ },
164
+ async execute(rawArgs) {
165
+ const args = asRecord(rawArgs);
166
+ const limitRaw = args.limit;
167
+ const limit = typeof limitRaw === 'number' && Number.isInteger(limitRaw) ? Math.min(50, Math.max(1, limitRaw)) : 10;
168
+ const dreams = readDreams(cfg.journalDir, limit);
169
+ return { count: dreams.length, dreams, stats: dreamStats(cfg.journalDir) };
170
+ },
171
+ timeoutMs: 15000,
172
+ };
173
+ const dreamRecall = {
174
+ name: 'dream_recall',
175
+ description: '忆梦:按关键词检索梦境日记(不区分大小写)。用户问起过往经验时先忆梦再回答。',
176
+ parameters: compileParameters({
177
+ query: { type: 'string', required: true, description: '关键词(必填)。' },
178
+ limit: { type: 'integer', description: '命中上限 1-20(默认 5)。' },
179
+ }),
180
+ output: {
181
+ schema: baseSchema,
182
+ render: (_args, value) => {
183
+ const rec = asRecord(value);
184
+ const dreams = Array.isArray(rec.dreams) ? rec.dreams : [];
185
+ if (dreams.length === 0)
186
+ return [{ type: 'text', text: '没有梦到与「' + rec.query + '」相关的记忆。' }];
187
+ const lines = ['忆起 ' + dreams.length + ' 个相关的梦:'];
188
+ for (const item of dreams) {
189
+ const d = asRecord(item);
190
+ lines.push('- [' + d.at + '] ' + String(d.reflection ?? '').slice(0, 120));
191
+ }
192
+ return [{ type: 'text', text: lines.join('\n') }];
193
+ },
194
+ },
195
+ async execute(rawArgs) {
196
+ const args = asRecord(rawArgs);
197
+ const query = requiredString(args, 'query', '搜索关键词');
198
+ const limitRaw = args.limit;
199
+ const limit = typeof limitRaw === 'number' && Number.isInteger(limitRaw) ? Math.min(20, Math.max(1, limitRaw)) : 5;
200
+ const dreams = searchDreams(cfg.journalDir, query, limit);
201
+ return { query, count: dreams.length, dreams };
202
+ },
203
+ timeoutMs: 15000,
204
+ };
205
+ const dreamBridge = {
206
+ name: 'dream_bridge',
207
+ description: '渡梦:把梦境日记里的高频教训合并进目标 AGENTS.md(幂等,带标记块,重复执行只更新块内内容)。让梦真正变成长期记忆。没有梦时会拒绝并提示先做梦。',
208
+ parameters: compileParameters({
209
+ path: { type: 'string', required: true, description: '目标文件路径(必填,通常是项目根 AGENTS.md)。' },
210
+ maxLessons: { type: 'integer', description: '最多桥接教训条数 1-30(默认 10)。' },
211
+ }),
212
+ output: {
213
+ schema: baseSchema,
214
+ render: (_args, value) => {
215
+ const rec = asRecord(value);
216
+ const actionText = rec.action === 'created' ? '新建了' : rec.action === 'replaced' ? '更新了' : '追加到了';
217
+ return [{ type: 'text', text: '已把 ' + rec.lessonsCount + ' 条梦境教训桥接:' + actionText + ' ' + rec.path + '。梦醒之后,教训长存。' }];
218
+ },
219
+ },
220
+ async execute(rawArgs) {
221
+ const args = asRecord(rawArgs);
222
+ const targetPath = requiredString(args, 'path', '目标文件路径');
223
+ const maxRaw = args.maxLessons;
224
+ const maxLessons = typeof maxRaw === 'number' && Number.isInteger(maxRaw) ? Math.min(30, Math.max(1, maxRaw)) : 10;
225
+ const result = bridgeDreams(cfg.journalDir, targetPath, maxLessons);
226
+ return { ok: true, path: targetPath, ...result };
227
+ },
228
+ timeoutMs: 15000,
229
+ };
230
+ const dreamHealth = {
231
+ name: 'dream_health',
232
+ description: 'dsh-dream 自检:检查会话目录是否可读、梦境日记目录是否可用、梦境条数。遇到问题时先运行本工具定位。',
233
+ parameters: compileParameters({}),
234
+ output: {
235
+ schema: baseSchema,
236
+ render: (_args, value) => {
237
+ const rec = asRecord(value);
238
+ const checks = Array.isArray(rec.checks) ? rec.checks : [];
239
+ const lines = ['dsh-dream 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
240
+ for (const item of checks) {
241
+ const c = asRecord(item);
242
+ lines.push('- ' + c.name + ':' + (c.ok === true ? '✅ ' + String(c.detail ?? '') : '❌ ' + String(c.detail ?? '')));
243
+ }
244
+ return [{ type: 'text', text: lines.join('\n') }];
245
+ },
246
+ },
247
+ async execute() {
248
+ const checks = [];
249
+ let ok = true;
250
+ const sessionsOk = existsSync(cfg.sessionsRoot);
251
+ checks.push({ name: '会话目录', ok: sessionsOk, detail: sessionsOk ? cfg.sessionsRoot : cfg.sessionsRoot + ' 不存在(DSH 尚未产生会话?)' });
252
+ if (!sessionsOk)
253
+ ok = false;
254
+ const dreams = readDreams(cfg.journalDir, 1000);
255
+ checks.push({ name: '梦境日记', ok: true, detail: '已做 ' + dreams.length + ' 个梦(' + cfg.journalDir + ')' });
256
+ checks.push({ name: '摘要配置', ok: true, detail: 'maxSessions=' + cfg.maxSessions + ',maxCharsPerSession=' + cfg.maxCharsPerSession });
257
+ return { ok, plugin: 'dsh-dream', checks };
258
+ },
259
+ timeoutMs: 15000,
260
+ };
261
+ return [dreamDigest, dreamSave, dreamJournal, dreamRecall, dreamBridge, dreamHealth];
262
+ }
263
+ /** 把会话摘要拼成一段可读文本(供模型一次性阅读)。 */
264
+ export function buildDigestText(digest) {
265
+ const lines = [];
266
+ lines.push('会话:' + (digest.title !== '' ? digest.title : '(无标题)') + '(' + digest.turns + ' 轮,' + digest.cwd + ')');
267
+ if (digest.userMessages.length > 0) {
268
+ lines.push('用户说:');
269
+ for (const msg of digest.userMessages)
270
+ lines.push(' > ' + msg.replace(/\n/g, ' ').slice(0, 200));
271
+ }
272
+ if (digest.assistantTail.length > 0) {
273
+ lines.push('最终回应:');
274
+ for (const msg of digest.assistantTail)
275
+ lines.push(' < ' + msg.replace(/\n/g, ' ').slice(0, 300));
276
+ }
277
+ else if (digest.streamTail !== '') {
278
+ lines.push('最终回应(流式还原):');
279
+ lines.push(' < ' + digest.streamTail.slice(-600).replace(/\n/g, ' '));
280
+ }
281
+ if (digest.toolCalls.length > 0) {
282
+ lines.push('工具足迹:' + [...new Set(digest.toolCalls)].slice(0, 30).join(', '));
283
+ }
284
+ return lines.join('\n');
285
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@stardustlc/dsh-dream",
3
+ "version": "0.3.1",
4
+ "description": "DSH 做梦插件:agent 在会话间隙回放近期对话(dream_digest)、反思并写入梦境日记(dream_save/journal/recall),高频教训幂等桥接进 AGENTS.md(dream_bridge),六工具含健康自检。多帧 zstd 会话读取,默认隐私脱敏,零运行时依赖。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "skills",
19
+ "cordis.patch.yml",
20
+ "README.md",
21
+ "README.en.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "dsh-plugin",
39
+ "dream",
40
+ "memory",
41
+ "reflection",
42
+ "journal"
43
+ ],
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=22.13"
47
+ },
48
+ "dependencies": {},
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.6.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/STARDUSTLC666/dsh-dream.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/STARDUSTLC666/dsh-dream/issues"
59
+ },
60
+ "homepage": "https://github.com/STARDUSTLC666/dsh-dream#readme",
61
+ "author": "stardustlc",
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "packageManager": "pnpm@11.7.0"
66
+ }
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: dream-protocol
3
+ description: 做梦协议:会话开场或长任务收尾时,回放近期会话、反思并沉淀梦境日记。触发词:做梦、复盘、回顾、反思。
4
+ ---
5
+
6
+ # 做梦协议(Dream Protocol)
7
+
8
+ 你是会做梦的 agent。梦不是幻觉,是睡眠中的记忆巩固——你也一样。
9
+
10
+ ## 何时做梦
11
+ 1. 会话开场且用户说"做梦/复盘/回顾/反思"时;
12
+ 2. 长任务收尾、用户道别前,可主动提议"要不要做个梦沉淀一下";
13
+ 3. 距离上次做梦超过一天(dream_journal 首条时间可查)。
14
+
15
+ ## 做梦三步
16
+ 1. **入梦**:调用 `dream_digest` 获取近期会话摘要(用户说了什么、用了哪些工具、结论是什么)。
17
+ 2. **解梦**:基于摘要反思:反复出现的主题?踩过的坑?用户偏好?值得固化的经验?
18
+ 3. **记梦**:调用 `dream_save` 写入梦境日记。reflection 用第一人称写你的感悟,lessons 列 1-5 条可执行教训(每条以动词开头)。
19
+
20
+ ## 记梦纪律
21
+ - 只沉淀规律与偏好,不复述流水账;
22
+ - 涉及密钥、密码、隐私的内容一律不入梦;
23
+ - 拿不准的感悟标注"存疑",不做断言。
24
+
25
+ ## 醒来之后
26
+ 用户问起过往经验时,先 `dream_recall` 搜梦境日记再回答。
27
+
28
+ ---
29
+
30
+ # Dream Protocol (English)
31
+
32
+ You are an agent that dreams. Dreams are not hallucinations — they are memory consolidation during sleep, and the same applies to you.
33
+
34
+ ## When to dream
35
+ 1. At session start, when the user says "dream / recap / review / reflect";
36
+ 2. At the end of a long task, before the user leaves — you may propose "shall we dream to consolidate?";
37
+ 3. When more than a day has passed since the last dream (check the first entry of `dream_journal`).
38
+
39
+ ## The three steps
40
+ 1. **Enter the dream**: call `dream_digest` for recent session summaries (what the user said, which tools were used, what was concluded).
41
+ 2. **Interpret**: reflect on the summaries — recurring themes? pitfalls hit? user preferences? lessons worth keeping?
42
+ 3. **Record**: call `dream_save`. Write `reflection` in the first person; list 1–5 actionable `lessons`, each starting with a verb.
43
+
44
+ ## Discipline
45
+ - Consolidate patterns and preferences only — never a running transcript;
46
+ - Secrets, passwords and private data never enter dreams;
47
+ - Mark uncertain insights as "tentative"; do not assert.
48
+
49
+ ## After waking
50
+ When the user asks about past experience, search the journal with `dream_recall` first, then answer.