Просмотр исходного кода

1. 搭建了 dsh 插件开发仓库,含 TypeScript 严格类型检查配置与 npm 测试脚本;
2. 编写了 dsh 插件开发技术指南,覆盖插件形态、工具开发、事件系统、打包分发与扩展点速查;
3. 编写了插件机制概念科普文档,以由浅入深的方式阐释服务、上下文、事件与可逆副作用等核心设计;
4. 实现了 prompt-journal 插件,将每条用户需求在执行前写入会话项目的 prompts.md 日志;
5. 实现了智能追加规则,按文件结尾状态自动补全分隔符,避免分隔线重复,支持手工维护与插件写入无缝混排;
6. 实现了沙箱感知写入,按会话工作区解析文件策略,从任意目录启动均可正确写入目标项目;
7. 实现了多来源过滤,仅记录真实用户输入,跳过插件注入、工具结果与子代理委派消息;
8. 实现了并发安全的追加写入,同一日志文件的读改写按目标串行化,多会话并发不互相覆盖;
9. 编写了插件行为测试脚本,覆盖消息过滤、追加格式、分隔归一、容错与配置校验等 15 组断言;
10. 制作了插件组合包清单与本地开发 overlay,支持 dsh plugin 安装与 --patch 两种加载方式;
11. 补充了 .gitignore,覆盖依赖、构建产物、日志、密钥、IDE 与双平台系统文件等常见忽略项;
12. 初始化了 Git 仓库并忽略 spec-workflow 工作目录,保留纯源码与文档入库。

weisijie 1 месяц назад
Сommit
3207411a27

+ 72 - 0
.gitignore

@@ -0,0 +1,72 @@
+# ── 本仓库特有 ────────────────────────────────────────────
+# 克隆的上游项目(仅作本地参考源码,勿提交)
+/deepseek-harness/
+
+# spec-workflow 工具的工作目录(规格过程文件,不入库)
+/.spec-workflow/
+
+# ── 依赖 ──────────────────────────────────────────────────
+node_modules/
+.pnpm-store/
+jspm_packages/
+
+# ── 构建产物(dsh 插件打包时输出到 lib/)──────────────────
+dist/
+build/
+out/
+lib/
+lib-types/
+*.tsbuildinfo
+
+# ── 测试与覆盖率 ──────────────────────────────────────────
+coverage/
+.nyc_output/
+.vitest-cache/
+
+# ── 日志与缓存 ────────────────────────────────────────────
+*.log
+logs/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+.eslintcache
+.stylelintcache
+
+# ── 环境变量与密钥 ────────────────────────────────────────
+.env
+.env.*
+!.env.example
+
+# ── IDE / 编辑器 ──────────────────────────────────────────
+.idea/
+.vscode/
+*.swp
+*.swo
+*.swn
+*~
+.history/
+
+# ── 操作系统(Windows / macOS)────────────────────────────
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+Desktop.ini
+$RECYCLE.BIN/
+*.lnk
+.DS_Store
+.AppleDouble/
+.LSOverride/
+._*
+.Spotlight-V100/
+.Trashes/
+
+# ── 压缩包与临时文件 ──────────────────────────────────────
+*.tar
+*.tar.gz
+*.tgz
+*.zip
+*.7z
+tmp/
+temp/

+ 149 - 0
docs/dsh-plugin-concepts.md

@@ -0,0 +1,149 @@
+# 理解 DeepSeek Harness:一篇插件机制的科普读物
+
+> 这是概念篇,讲"为什么这样设计"。代码与命令细节请看同目录的《dsh-plugin-guide.md》(技术参考篇)。
+>
+> 阅读本文不需要编程背景,但读完后你应该能明白:一个插件在 dsh 里是什么、靠什么活下去、如何被别人使用。
+
+## 一、先搞清楚:Agent Harness 是什么
+
+大语言模型很聪明,但它像一颗**泡在营养液里的大脑**——没有手,摸不到文件;没有腿,跑不了命令;没有本子,记不住昨天做了什么。
+
+**Agent Harness(智能体运行框架)就是给这颗大脑配上的"身体和工位"**:一个能读文件、执行命令、调用 API、记录对话历史的工作环境。模型负责思考,harness 负责让思考落地。
+
+DeepSeek Harness(简称 dsh)就是 DeepSeek 开源的这样一套"工位"。你在终端或网页里和它对话、让它改代码查资料时,背后支撑这一切的就是它。
+
+## 二、dsh 最特别的决定:没有"内核"
+
+想象两种商场:
+
+**传统商场**:有一栋不可动摇的主楼(内核),商户(插件)只能在旁边的铺位营业。你想改造主楼的大堂?对不起,那是开发商的地盘,得动整栋楼的图纸。
+
+**dsh 商场**:**没有主楼**。保安室、配电房、广播站、连"商场管理办公室"本身,全都是一间间普通的铺位。每一间都可以退租、可以换成隔壁竞品、可以在隔壁再开一家。
+
+这就是 dsh 的口号——**一切皆插件**:
+
+- 对接模型的代码?插件。
+- 工具清单?插件注册出来的。
+- 聊天记录的存储?插件。
+- 甚至"智能体循环"本身(模型思考 → 调工具 → 再思考的那个循环)?也是插件。
+
+为什么这么执着?因为**"每一部分都能被换掉"意味着每一部分都能被定制**。举例:把"文件系统"插件换成指向远程沙箱的版本,那么所有依赖文件系统的东西——Bash 命令、终端、代码分析——会自动跟着搬进沙箱,一行都不用重写。能力像插头一样换了个插座,电器们毫无感知。
+
+## 三、那么,"插件"到底是什么?
+
+剥掉所有术语,一个插件就是一份**三句话的自我介绍**:
+
+```ts
+export const name = 'hello-plugin'   // 我叫什么
+export const inject = ['tools']      // 我需要什么(可以不填)
+export function apply(ctx) { ... }   // 入驻后我要做什么
+```
+
+把它想象成**入驻商场的租户**:
+
+1. **签合同时报名字**(`name`);
+2. **在合同里写明"我需要水电和网络"**(`inject`)——商场保证这些就位之前,不会让你进场装修;
+3. **进场后开始布置**(`apply`)——挂招牌、摆货架,也就是把你的能力"注册"出去;
+4. **退租时一切自动恢复原样**——这一点下一节细讲,它是整个设计的魔法所在。
+
+插件可以简单到一个函数,也可以复杂到一个类。形态不重要,重要的是这份"自我介绍"的契约。
+
+## 四、两个基石概念:服务与上下文
+
+### 服务:插在墙上的标准插座
+
+插件之间怎么合作?不是互相抄微信,而是通过**服务**。
+
+dsh 里每个能力都占一个公开的、有名字的位置——像一个标准的**墙面插座**:
+
+- `ctx.tools` —— 工具登记处
+- `ctx.llm` —— 模型接线总机
+- `ctx.fs` —— 文件系统接口
+- `ctx.sessions` —— 会话档案室
+
+想用工具?你不需要认识"工具部"的任何一个人,只要找到 `tools` 这个插座。**依赖名字,而非依赖具体的人**——这是解耦的第一课:插座后面接的是谁,随时可以换。
+
+### 上下文(ctx):租户的钥匙牌
+
+每个插件入驻时,商场发给它一块**钥匙牌**(`ctx`)。用这块牌子能做的一切操作——注册工具、监听事件、占用资源——都被记账。为什么要记账?为了下一节的主角。
+
+## 五、魔法所在:可逆的副作用
+
+传统插件系统最常见的烂尾方式:插件装了又卸,留下几个再也杀不掉的定时器、几个没人认领的监听器,系统越跑越脏,最后只能重启。
+
+dsh 的解法朴素而彻底:**你在钥匙牌(ctx)上做的每一个注册,都会被记账;退租时按账单逐条撤销。**
+
+- 注册了一个工具?卸载时自动注销。
+- 监听了一个事件?自动移除。
+- 起了一个定时器?用 `ctx.effect(() => 返回清理函数)` 告诉商场怎么关掉它。
+
+带来的直接后果是:**插件可以随时加载、随时卸载、随时热替换**——改一行配置,旧的退场、新的进场,不留残渣。上一节说"一切皆可替换",而"可逆的副作用"就是让"替换"敢真的发生而不留后患的地基。
+
+## 六、插件之间怎么说话:事件
+
+插座(服务)解决"找到对方",但很多场景不适合一对一喊话:
+
+- 我只是想**知道**发生了什么("有工具刚执行完");
+- 我想**拦截**一件事("这次文件写入,先问过用户没有?");
+- 很多插件都想**加工**同一份数据("请求发出去之前,每人加一点自己的料")。
+
+dsh 用**事件**覆盖这些场景,几种模式各像一种生活场景:
+
+| 模式 | 生活类比 | 特点 |
+|---|---|---|
+| `emit` | 小区广播 | 谁想听谁听,说完就完 |
+| `bail` | 举手认领 | 排队问"谁能处理?",第一个举手的定案 |
+| `waterfall` | 流水线 | 每站加工一下**必须传给下一站**;不传,货就被扣下(= 拦截) |
+| `serial` | 依次签收 | 按顺序过手,每步等上一步 |
+
+其中 `waterfall` 值得多看一眼,因为**"拦截"就是这么实现的**:想做一个权限网关?在流水线上占一站,检查到危险操作就不把货传下去(不调用 `next()`),整个请求到此为止。想做一个观察者?加工完原样传下去即可。一套机制,两种角色。
+
+另一个容易混淆的点:dsh 里有两类"事件"。**实时事件**(如 `agent/*`、`tools/*`)像广播,听过就过了;**会话事件**(如每轮对话、每次工具调用的记录)像**台账**,一笔笔记进日志,回放、恢复、摘要全都从这本台账重建。原则一句话:**凡是模型看过的东西,必须能从台账里查回来。**
+
+## 七、插件怎么"给模型装上手":工具
+
+模型终究要通过某种方式操作世界,这个界面就是**工具**。一个工具是一张标准格式的"操作卡":
+
+- **说明书**(description):写给模型看的——这张卡是干嘛的;
+- **申请表**(parameters):模型按格式填参数;
+- **执行器**(execute):真正干活的部分;
+- **回执**(output):干完之后返回什么。
+
+这里藏着 dsh 一个很克制的设计:**回执必须是规范的数据,而不是一段话**。比如"搜索文件"工具返回的是结构化的匹配列表,而不是一段"我帮你找到了三个文件,它们分别是……"的散文。原因:散文是给人看的,数据是给程序用的;从数据生成散文容易,反过来就要靠猜。人类可读的展示(UI 卡片)是独立的另一层,不混进模型的结果里。
+
+还有两个理念值得一提:
+
+- **权限不写死在工具里**。"这个操作危不危险、要不要先问用户"属于流水线上的策略站(第六节的 waterfall),工具本身只管干活。工具换到别的环境,策略跟着环境走。
+- **说明书自动生效**。工具注册后,它的说明书会自动汇入给模型的提示词——模型"天生"就知道自己有哪些卡可用,不需要额外接线。
+
+## 八、插件怎么组合成一整套系统
+
+单个插件好理解了,那一个跑起来的 dsh 是怎么拼出来的?答案是**分层穿搭**:
+
+1. **组合包(bundle)**是一袋配好的料:一个 npm 包,里面除了代码还附一张**配置单**(声明"我包含哪些插件、各带什么参数")。它是最小的分发单位——你写好一个袋子,别人拎走就能用。
+2. **profile**是一套完整穿搭:按顺序把 `dsh-base`(基础款:模型对接、工具、存储……)和各个组合包一层层穿上,再套上用户自己的 `cordis.patch.yml`(个人偏好层)。
+3. **--patch overlay**是出门前临时加的外套:不改任何文件,命令行上指定一下就生效——本地开发插件时天天穿它。
+
+叠加规则简单粗暴:**后穿的覆盖先穿的,而且是整件替换、不做缝缝补补**。这带来一个舒服的生态关系:插件作者提供合理的默认值,用户永远可以在自己的层里整行覆盖,谁也不用求谁改代码。
+
+想看自己身上到底穿了什么?一条 `--dump-config` 命令展开整套配置树,每一层、每一行都清清楚楚。
+
+## 九、插件怎么到达用户手里:分发与信任
+
+三种主要途径,本质区别在**"用户拿到的是成品还是原料"**:
+
+- **npm 发布 / tarball**:成品。作者预先构建好,用户一条命令安装,无需任何额外授权。
+- **GitHub 安装**:原料。拉下来的是源码,需要在用户机器上现场构建——这意味着用户必须**显式授权**"允许这个包在安装时执行构建脚本"。
+
+这个授权设计不是流程刁难,而是一次诚实的风险告知:允许构建脚本,等于允许陌生代码在你机器上、且在任何沙箱之外执行。所以官方建议:只授权给看过源码的包,并锁定到具体 commit。**信任应该是一个显式的动作,而不是默认发生的事。**
+
+## 十、把整张地图串起来
+
+最后用一段话把所有概念连成线:
+
+> 模型是大脑,**harness** 是工位。工位上的一切设备都是**插件**——它们靠**服务**(标准插座)找到彼此,靠**依赖声明**决定开工顺序,靠**事件**(广播与流水线)协作与拦截,靠**可逆的副作用**做到即插即拔不留渣。模型通过**工具**(操作卡)驱动这些设备,操作卡的说明书自动汇入模型的视野。一个个插件打成**组合包**分发,用户按层叠穿成自己的 **profile**——于是,一套完全属于你自己的智能体工位就成型了。
+
+如果这张地图让你产生了"我想试试"的念头,接下来:
+
+1. **动手写第一个插件**:上游仓库 `docs/user/develop/basic/index.zh.md`(半小时教程,一个文件跑起来);
+2. **查阅具体契约**:回到本目录的《dsh-plugin-guide.md》,那里有本文每个概念的精确语法和完整约定。

+ 307 - 0
docs/dsh-plugin-guide.md

@@ -0,0 +1,307 @@
+# DeepSeek Harness (dsh) 插件创作机制指南
+
+> 本文档基于上游仓库 `deepseek-ai/deepseek-harness`(克隆于本目录 `deepseek-harness/`,已加入 `.gitignore`)的官方文档整理,作为本仓库(dsh-plugins)开发 dsh 插件的参考。
+>
+> 上游处于 developer preview 阶段,**存在破坏性变更风险**。本文所有路径引用均相对于 `deepseek-harness/`。
+
+## 1. 项目概览
+
+DeepSeek Harness(`dsh`)是 DeepSeek AI 开发的开源 Agent Harness(智能体运行框架),底层基于 [Cordis](https://github.com/cordiverse/cordis) 插件框架(以 vendor 方式引入为 `@deepseek-ai/cordis`)。
+
+核心架构理念:**一切皆插件**。模型适配器、工具注册表、会话日志、甚至 agent loop 本身都是插件,因此每一部分都可以从配置替换。不存在需要打补丁的特权内核——扩展 dsh 的方式是把插件挂载到其他插件旁边。
+
+关键目录:
+
+| 路径 | 内容 |
+|---|---|
+| `docs/user/develop/` | **面向插件作者的教程**(入门、工具、配置、发布、框架) |
+| `docs/cookbook/` | 扩展实操手册(添加包/工具/LLM 适配器/Chat 节点/设置卡片) |
+| `docs/architecture.zh.md` | 整体架构与扩展点归属映射 |
+| `docs/cordis-primer.zh.md` | Cordis 框架入门 |
+| `packages/` | 全部核心包(monorepo,pnpm workspace) |
+| `examples/` | 可运行示例(mcp-memory、headless-agent、web-schedule 等) |
+
+## 2. Cordis 五个核心概念
+
+理解插件机制的基础(详见 `docs/cordis-primer.zh.md`):
+
+1. **插件是实现 Service 的对象**——可以是带可选 `inject` 和 `apply(ctx)` 字段的函数,也可以是 `Service` 子类,生命周期由 Cordis 管理。
+2. **上下文是服务的容器**——一个服务占据一个稳定的 `ctx.<key>`(如 `ctx.tools`、`ctx.llm`、`ctx.sessions`);其他插件通过 key 查找服务,而非导入具体实现。
+3. **通过 `inject` 声明服务依赖**——插件等待所需服务就绪后才启动;加载顺序由服务依赖表达,无需手动编排。
+4. **类型化事件用于通信**——通过 TypeScript 声明合并注册事件名,以 `emit`、`waterfall`、`parallel`、`serial`(另有 `bail`)分发。
+5. **注册是可逆的副作用**——工具、监听器等通过 `ctx.effect()` 或 `ctx.on()` 安装,reload/teardown 时自动撤销。
+
+## 3. 插件的基本形态
+
+插件是导出 `apply` 函数的 TypeScript/JavaScript 模块(详见 `docs/user/develop/basic/index.zh.md`):
+
+```ts
+import type { Context } from '@deepseek-ai/cordis'
+
+export const name = 'hello-plugin'
+export const inject = ['tools']  // 可选:声明依赖的服务,就绪后才执行 apply
+
+export function apply(ctx: Context) {
+  // ctx.tools 已就绪,在此注册能力
+}
+```
+
+三种等价形态:
+
+- **函数形式**(最常用):如上。
+- **对象形式**:`export default { name, inject, apply(ctx) {} }`。
+- **类形式**(需要向其他插件提供服务时):`export default class MyService extends Service`,构造函数中 `super(ctx, 'myService')` 占据 `ctx.myService`。
+
+### 生命周期(Fiber 状态机)
+
+```
+PENDING → LOADING → ACTIVE
+                 ↘ FAILED
+ACTIVE → UNLOADING → DISPOSED
+```
+
+- 声明 `inject` 的插件在依赖服务就绪后加载;依赖消失时自动卸载,恢复后重新加载。
+- **自动清理**:`ctx.on()`、`ctx.tools.register()`、`ctx.llm.registerAdapter()` 等注册随插件卸载自动撤销;自定义资源用 `ctx.effect(() => disposer)` 声明清理逻辑。
+- 处置器按注册逆序调用,但异步处置器并发执行;有顺序依赖的清理须放进同一个 effect。
+- 支持 HMR:修改 `cordis.yml` 配置或(通过 `cordis-plugin-hmr`)源文件会卸载旧实例、加载新实例。
+
+## 4. 插件配置
+
+导出 `Config`(Schemastery schema)即可让用户在 `cordis.yml` 中传配置(详见 `docs/user/develop/basic/config.zh.md`):
+
+```ts
+import Schema from '@deepseek-ai/schemastery'
+
+export interface Config {
+  greeting: string
+  maxRetries: number
+}
+
+export const Config: Schema<Config> = Schema.object({
+  greeting: Schema.string().default('Hello'),
+  maxRetries: Schema.number().default(3),
+})
+
+export function apply(ctx: Context, config: Config) {
+  console.log(config.greeting)
+}
+```
+
+设计约定:
+
+- **无硬编码可调参数**:凡不同部署可能需要不同值的参数,必须定义为配置字段。检验标准:能否不改代码、只改 `cordis.yml` 就改变该值?
+- **配置错误要响亮**:在 schema 中表达完备约束,使无效配置在加载时即失败。
+- 不要导出普通对象作为 `Config`,必须满足 Standard Schema 接口。
+
+## 5. 工具开发
+
+面向模型的工具通过 `defineTool` + `ctx.tools.register()` 注册(详见 `docs/user/develop/basic/tool.zh.md` 与 `docs/cookbook/adding-a-tool.zh.md`):
+
+```ts
+import type { Context } from '@deepseek-ai/cordis'
+import { defineTool } from '@deepseek-ai/dsh-tools'
+
+export const name = 'my-tool'
+export const inject = ['tools']
+
+export function apply(ctx: Context) {
+  ctx.tools.register(defineTool({
+    name: 'read_file',
+    description: 'Read a file from disk.',       // 模型可见描述
+    parameters: {
+      path: { type: 'string', required: true, description: 'Absolute path' },
+      limit: { type: 'number' },                  // 默认可选
+    },
+    output: {
+      schema: { type: 'string' },                 // 规范 JSON 值的 schema
+      render: (_args, value) => [{ type: 'text', text: value }],  // 模型可见内容
+    },
+    async execute(args, exec) {
+      // args 已按 schema 自动校验并推导类型
+      // exec.signal 用于取消;exec.agent 可发送异步通知
+      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
+    },
+  }))
+}
+```
+
+核心约定:
+
+- **参数自动校验**:`defineTool` 在 `execute` 前按 `parameters` 校验模型生成的参数;schema DSL 表达不了的约束(非空、正数、跨字段)需手动检查。
+- **返回规范 JSON 值**:`execute` 只返回 `output.schema` 声明的值;不要返回内容块或迫使调用方解析自然语言。抛异常或返回无效值即 `isError`。
+- **遵守 `exec.signal`**:信号触发时取消进行中的工作。
+- **schema 自动流入系统提示词**:注册即生效,无需额外集成;Code Mode 中每个可见工具自动可经 `await tools.<name>(args)` 调用。
+- **UI 卡片独立声明**(可选):`presentCall(args)` / `presentResult(args, result)` 返回渲染意图(`generic` / `terminal` / `diff` / `search` / `web` 卡片)。必须是纯函数(实时流式与日志回放都会执行,禁止 I/O、时钟、随机数)。
+- **后台任务**:producer 配置 `run_in_background`,用 `ctx.jobs.start({ kind, label, owner: exec.agent, run })` 注册;返回 `{ kind: 'background', jobId }` 类型的规范句柄。
+- **执行策略不内建到工具**:用 `tools/pre-execute`(允许/拒绝/询问)、`tools/execute`(截止时间/重试/指标)、`tools/post-execute`(改写展示或结果)、`tools/result`(观测不可变结果)等事件实现。
+
+生产级参考实现:`packages/shell/tool-bash`(三包结构)、`packages/core/fs` 中的工具。
+
+## 6. 事件系统
+
+事件是插件间松耦合通信的核心机制(详见 `docs/user/develop/framework/events.zh.md`):
+
+| 模式 | 语义 |
+|---|---|
+| `emit` | 广播;监听器按注册顺序同步观察,无返回值 |
+| `bail` | 短路;第一个非空返回值成为最终结果 |
+| `serial` | 顺序执行,await 异步结果,非空返回值终止后续 |
+| `waterfall` | 中间件链;监听器收到 `(...args, next)`,**必须调用 `next()`** 才能委托下游,不调用即短路(用于拦截/网关) |
+| `parallel` | 所有监听器并行观察 |
+
+- 事件名遵循 `namespace/action` 约定(如 `agent/pre-step`、`tools/result`、`session/event`)。
+- 通过声明合并获得类型安全:
+
+```ts
+declare module '@deepseek-ai/cordis' {
+  interface Events {
+    'my-plugin/ready': (payload: { id: string }) => void
+  }
+}
+```
+
+- 注意区分:`turn/*`、`step/*`、`tool/call`、`tool/result` 等是**持久化会话事件类型**(日志事实),观察它们要监听 `session/event` 并检查 `event.type`;`agent/*`、`tools/*` 才是实时 Cordis 事件。
+- 单决策事件(如 `agent/pre-step`、`agent/request`、`llm/stream`、`tools/*` 三事件)是 waterfall;`agent/turn-stopping` 是 serial。
+
+## 7. 打包与分发
+
+两个核心概念(详见 `docs/user/develop/basic/publish.zh.md`):
+
+- **组合包(bundle)**:附带一个配置层的 npm 包,是你编写并分发的单位。`package.json` 中 `dsh.bundle` 指向其 patch 文件。
+- **profile**:位于 `$DSH_HOME/profiles/<name>` 的可启动组装目录,`dsh.profile.bundles` 按序列出组合包。由 `dsh plugin` 命令维护,无需手写。
+
+### 组合包最小结构
+
+```
+hello-plugin/
+├── package.json       # 声明 dsh.bundle
+├── cordis.patch.yml   # 被列入 profile 时应用的配置层
+└── index.js           # 插件入口
+```
+
+```json
+{
+  "name": "dsh-hello-plugin",
+  "version": "0.1.0",
+  "type": "module",
+  "main": "index.js",
+  "files": ["index.js", "cordis.patch.yml"],
+  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
+}
+```
+
+`cordis.patch.yml`(插件行按**包名**引用,让 Node 模块解析找到已安装代码):
+
+```yaml
+- insert:
+    - id: hello
+      name: dsh-hello-plugin
+```
+
+### 本地开发(--patch overlay)
+
+无需打包,直接以绝对路径插入本地源码:
+
+```yaml
+- insert:
+    - id: hello
+      name: '/absolute/path/to/my-plugin/src/my-plugin.ts'
+```
+
+```sh
+dsh web --patch ./cordis.yml   # 仓库源码内用 pnpm dsh web
+```
+
+### 安装与加载顺序
+
+```sh
+dsh plugin --profile demo add ./hello-plugin      # 本地路径
+dsh plugin --profile demo add github:you/hello-plugin   # git(需 prepare 脚本 + allowBuilds 授权)
+dsh plugin --profile demo add your-package        # npm(预构建,无需授权)
+dsh plugin --profile demo remove dsh-hello-plugin
+dsh --profile demo --dump-config                  # 查看实际生效的配置树
+```
+
+生效配置按以下顺序逐层叠加(后应用的层按行胜出,patch 替换目标行**整个** config,不做深度合并):
+
+1. profile 的 `dsh.profile.bundles` 所列各组合包 patch(按列表顺序,首个为 `@deepseek-ai/dsh-base`)
+2. profile 自己的 `cordis.patch.yml`
+3. home 级 `$DSH_HOME/cordis.patch.yml`
+4. 各 `--patch <path>` overlay(按 argv 顺序)
+
+推论:你的 patch 可按 `id` 覆盖前面层的行(须重述该行全部键);用户可在自己 profile 的 patch 中覆盖你的行(因此优先给出合理默认值)。
+
+### git 安装的构建授权
+
+git 安装拉取源码而非构建产物,作者需提供自包含的 `prepare` 脚本(参考 [turtle-ui](https://github.com/deepseek-harness/turtle-ui));用户需在 profile 的 `pnpm-workspace.yaml` 中授权(`allowBuilds: { dsh-hello-plugin: true }`)——这等于允许该包代码在安装时于本机执行,只对可信源码授权并锁定 commit。免授权的替代:发布到 npm(预构建 `lib/`)或交付 tarball(`pnpm pack`)。
+
+## 8. 扩展点速查表
+
+新行为附加到已有扩展点(完整版见 `docs/architecture.zh.md` § 新行为的归属位置):
+
+| 目标 | 机制 |
+|---|---|
+| 添加模型提供方 | 在 `ctx.llm` 上注册适配器 |
+| 添加面向模型的能力(工具) | `ctx.tools.register()`;schema 自动加入提示词组装 |
+| 添加用户命令(无需模型轮次) | `ctx.commands` |
+| 添加后台工作 | `ctx.jobs`;`job_*` 工具收集/停止 |
+| 添加 shell / 终端执行 | 注册 `ctx.shell` 后端 / `ctx.terminals` 后端 |
+| 添加文件系统访问或策略 | 注册 `ctx.fs` 提供方,或监听 `fs/*` 事件 |
+| 限制所启动的进程 | `ctx.sandbox` 后端 |
+| 拦截请求、工具或轮次 | `agent/*` 或 `tools/*` 事件;`agent/turn-stopping` 停止轮次 |
+| 添加模型可见上下文 | `agent.inject()`(落入下一次获准的请求) |
+| 添加 UI / Web Chat 节点 | 驱动 `ctx.agents` 并从 `session/event` 渲染;注册 `ConversationNodeDefinition` |
+| 添加持久会话状态 | 扩展 `SessionEventMap`,从日志渲染和回放 |
+| 生成会话标题 | 注册唯一的 `ctx.sessionTitle` 提供方 |
+| fork 活跃会话 | `ctx.sessions.fork(source, boundary?)` |
+| 将注册项限定到单个 agent | 使用该 agent 的 `agent.ctx` |
+
+### 能力 seam(可替换能力)
+
+一个 **seam** 包含三种角色,可按需拆分为不同包:
+
+- **Service Definition**:声明接口
+- **Service Provider**:实现接口(替换提供方即改变整个产品,如把 `fs`/进程指向远程沙箱)
+- **Consumer**:使用能力(通常是面向模型的工具)
+
+### 命名与包拓扑
+
+- 单一 engine/runtime/policy/controller/resolver/store 用单数 `ctx` key;registry 用复数。
+- 接口包用能力名称;实现包加机制/协议/环境/厂商限定词。
+- 向上游贡献 workspace 包的完整清单见 `docs/cookbook/adding-a-package.zh.md`(本仓库开发树外插件无需遵循其 workspace 不变式,但角色命名规范值得借鉴)。
+
+## 9. 关键工作流速记
+
+```sh
+# 克隆并从源码运行(探索/调试上游)
+git clone https://github.com/deepseek-ai/deepseek-harness.git
+cd deepseek-harness && pnpm install && pnpm run build
+pnpm dsh web --patch ./my-overlay/cordis.yml
+
+# 快速验证插件层
+dsh --profile <name> --dump-config
+
+# 发布插件
+pnpm build && pnpm publish     # 预构建 lib/ 后发布 npm
+```
+
+## 10. 上游文档索引
+
+| 主题 | 路径(相对 `deepseek-harness/`) |
+|---|---|
+| 第一个插件 | `docs/user/develop/basic/index.zh.md` |
+| 开发一个工具(教程) | `docs/user/develop/basic/tool.zh.md` |
+| 插件配置 | `docs/user/develop/basic/config.zh.md` |
+| 打包与安装插件 | `docs/user/develop/basic/publish.zh.md` |
+| 插件与生命周期 | `docs/user/develop/framework/index.zh.md` |
+| 事件系统 | `docs/user/develop/framework/events.zh.md` |
+| 服务与依赖 | `docs/user/develop/framework/service.zh.md` |
+| 工具编写参考(权威) | `docs/cookbook/adding-a-tool.zh.md` |
+| LLM 适配器 | `docs/user/develop/practice/llm-adapter.zh.md`、`docs/cookbook/adding-an-llm-adapter.zh.md` |
+| 架构总览 | `docs/architecture.zh.md` |
+| Cordis 入门 / 教程 | `docs/cordis-primer.zh.md`、`docs/cordis-tutorial/index.zh.md` |
+| 扩展实操手册 | `docs/cookbook/extension-cookbook.zh.md` |
+| 工具目录 / 配置目录(生成) | `docs/tool-catalog.zh.md`、`docs/config-catalog.zh.md` |
+| 示例集 | `examples/README.zh.md` |
+
+社区:GitHub Discussions;插件仓库可打 `dsh-plugin` topic 提升可发现性;[Discord](https://discord.gg/Ycq5dCaS4)。

+ 289 - 0
package-lock.json

@@ -0,0 +1,289 @@
+{
+  "name": "dsh-plugins",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "dsh-plugins",
+      "devDependencies": {
+        "@deepseek-ai/cordis": "4.0.1",
+        "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
+        "@deepseek-ai/dsh-fs": "0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
+        "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-session": "0.1.1-rc.2",
+        "@deepseek-ai/schemastery": "^3.18.1",
+        "@types/node": "^22",
+        "typescript": "^5.6"
+      }
+    },
+    "node_modules/@deepseek-ai/cordis": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis/-/cordis-4.0.1.tgz",
+      "integrity": "sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@deepseek-ai/cosmokit": "^1.8.2",
+        "@standard-schema/spec": "^1.1.0"
+      },
+      "bin": {
+        "cordis": "bin.js"
+      },
+      "peerDependencies": {
+        "@deepseek-ai/cordis-plugin-include": "^1.0.6",
+        "@deepseek-ai/cordis-plugin-loader": "^1.0.2"
+      },
+      "peerDependenciesMeta": {
+        "@deepseek-ai/cordis-plugin-include": {
+          "optional": true
+        },
+        "@deepseek-ai/cordis-plugin-loader": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@deepseek-ai/cosmokit": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/cosmokit/-/cosmokit-1.8.2.tgz",
+      "integrity": "sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@deepseek-ai/dsh-agent": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent/-/dsh-agent-0.1.1-rc.2.tgz",
+      "integrity": "sha512-cC7lnJe7JgPFcreNXxcxLMxQd78LnpVO9ZXROjZsGRQN1zGH6i/DduI892F1am85IfzzO+XTxMwwUHmfwamb0g==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-scope": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-attachment": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-attachment/-/dsh-attachment-0.1.1-rc.2.tgz",
+      "integrity": "sha512-rCYAt8QsawP1yfDCU7XxNwYT/XWvyFsxYrkwhLLkdfW83QVD0CQHizSkTQE7RFX74nKUD1z3sTLfnLr7xneArw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-brand": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-brand/-/dsh-brand-0.1.1-rc.2.tgz",
+      "integrity": "sha512-8vXsAXoUdzKAgvd/E9DgyT6HKmR6ZM4rtJ3fs/XoJ6n2kBk4tWil8Lv+jxCMWJeMOnTJF55gu2+NWQ3ECFPtJw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-fs": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-fs/-/dsh-fs-0.1.1-rc.2.tgz",
+      "integrity": "sha512-8j+6MffvCHATLQrhAVfc9rKyunKu/O7mjjJzmdsUSdID7V4iUYMwqPamhlAyI+tfohZu/vcforKzCRIZGmCYug==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-invariants": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.1.1-rc.2.tgz",
+      "integrity": "sha512-l+1Om/EDFyMjhgSuEx2WDLLA2fia/+ga9mBTCoT/MMslsnWaK5G0/lWwbwlTBSaJ6OfmYc3DuBgox8DbgIGHRQ==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@deepseek-ai/schemastery": "^3.18.1"
+      },
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-llm": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm/-/dsh-llm-0.1.1-rc.2.tgz",
+      "integrity": "sha512-ASJfjIdZbIXvLwi3rGo+eZb/GxMVV/WO5/XVD3B96mT8EIzrlw3+nMR6/CvmJVzcycKQ2XN0wj7jD6TasPRySA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@deepseek-ai/schemastery": "^3.18.1"
+      },
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-timeout": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-sandbox": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox/-/dsh-sandbox-0.1.1-rc.2.tgz",
+      "integrity": "sha512-rnO2RqZ+ycpwrXrXlMcrhWAICdui3ZVTjNQ8eZrOPE18hAbX3tw0nLFq26sBjMSnBfDQHNZ4VaFpt0p8qhkPWQ==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-sandbox-policy": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox-policy/-/dsh-sandbox-policy-0.1.1-rc.2.tgz",
+      "integrity": "sha512-cpoIUxCzpZJDTMXVt9gS+qgWEDAWf6rIe715uY1NF0ROoiEXPlmToLsHLF+4pXTW3wWWzpGVswO0bPYEKrQr3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@deepseek-ai/schemastery": "^3.18.1"
+      },
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-scope": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-scope/-/dsh-scope-0.1.1-rc.2.tgz",
+      "integrity": "sha512-Xy3ejL6dwVSluZL7XOWy76ya4pCw1uHwxodDK4O9XiQUiUV4FBXnt0aNJUtMeAFN0c1YujxxCmRniMvuuNn1Nw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-session": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session/-/dsh-session-0.1.1-rc.2.tgz",
+      "integrity": "sha512-4/cv6X9HPhm47eyRhCu/WZwzrtJKegk5J+0xaxcZ9i8S0smdxP57tqy8a0jkSshLQn7BzMFxneQrlYExrLrDhQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-scope": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-system-prompt": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-system-prompt/-/dsh-system-prompt-0.1.1-rc.2.tgz",
+      "integrity": "sha512-on4hjAlYI5uX9q7Sf95YkMMBVe6heywtA/H50ksrIMUub8U2B98hO9iQpHhjwIO1F1vu+5pLcPvRr6yUGGmtXQ==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@deepseek-ai/schemastery": "^3.18.1"
+      },
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
+        "@deepseek-ai/dsh-scope": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-timeout": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.1.1-rc.2.tgz",
+      "integrity": "sha512-RrouVgU3G5gXr9zHhpThkMG6YKdcRJzXXdPm1dq3ioBxbvxlfMSfNY4tN8lWMJxLyGtvWkPra0HQX+YWxvdOOA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/dsh-typert-protocol": {
+      "version": "0.1.1-rc.2",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-protocol/-/dsh-typert-protocol-0.1.1-rc.2.tgz",
+      "integrity": "sha512-lxBssDc5Pz1qBE5kuIyaArA7AvIPq9rpaVclylodiSzVJe95e2xruBg73tflyjtd8y00toet+DLgQ6tSSsq6Kw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@deepseek-ai/cordis": "^4.0.1",
+        "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
+      }
+    },
+    "node_modules/@deepseek-ai/schemastery": {
+      "version": "3.18.1",
+      "resolved": "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz",
+      "integrity": "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@deepseek-ai/cosmokit": "^1.8.2",
+        "@standard-schema/spec": "^1.1.0"
+      }
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "22.20.1",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz",
+      "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}

+ 20 - 0
package.json

@@ -0,0 +1,20 @@
+{
+  "name": "dsh-plugins",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "typecheck": "tsc -p .",
+    "test": "node prompt-journal/behavior-check.mjs"
+  },
+  "devDependencies": {
+    "@deepseek-ai/cordis": "4.0.1",
+    "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-fs": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.2",
+    "@deepseek-ai/dsh-session": "0.1.1-rc.2",
+    "@deepseek-ai/schemastery": "^3.18.1",
+    "@types/node": "^22",
+    "typescript": "^5.6"
+  }
+}

+ 81 - 0
prompt-journal/README.md

@@ -0,0 +1,81 @@
+# prompt-journal
+
+把你在 dsh 中发送的每一条需求,在**执行前**追加写入当前项目(会话 cwd)的
+`prompts.md`,条目之间以 `\n\n---\n\n` 分隔。它是一个被动的观察者:只记录,不改变
+任何消息、不拦截任何步骤;写入失败只打一条警告,绝不影响 agent 运行。
+
+## 记录什么
+
+- 只记录**真实用户输入**(`source.kind === 'user'` 的消息):包括排队发送、
+  运行中途插入的 steering 输入,每条消息记为一个条目。
+- 不记录:插件注入的上下文(如 agent-instructions、time-context)、工具结果、
+  子 agent(subagent)收到的委派任务。
+
+## 配置
+
+| 字段 | 默认值 | 说明 |
+|---|---|---|
+| `file` | `prompts.md` | 日志文件,相对会话 cwd(也可用绝对路径) |
+| `separator` | `'\n\n---\n\n'` | 两个条目之间的分隔符 |
+
+文件不存在时自动创建;已存在的文件只在末尾追加(去除尾部空白后拼接),已有内容
+不会被改写。若部署没有注册 `fs` 提供方,插件以 no-op 挂载并警告一次。
+
+## 安装
+
+### 方式一:安装进 profile(推荐)
+
+```sh
+dsh plugin --profile web add /d/projects/dsh-plugins/prompt-journal
+dsh --profile web --dump-config   # 确认 prompt-journal 出现在配置树中
+```
+
+### 方式二:--patch overlay(本地开发,不安装)
+
+写一个 `cordis.yml`:
+
+```yaml
+- insert:
+    - id: prompt-journal
+      # Windows 下绝对路径必须写成 file:// URL
+      name: 'file:///D:/projects/dsh-plugins/prompt-journal/index.ts'
+```
+
+启动(需在仓库根目录先执行 `npm install`,插件的运行时依赖
+`@deepseek-ai/schemastery` 从根 node_modules 解析):
+
+```sh
+dsh web --patch ./cordis.yml
+```
+
+随后在任意项目里发的需求都会写入该项目的 `prompts.md`。
+
+## 实现要点
+
+- 监听 `agent/pre-step`(waterfall),以 `prepend` 占据链头:先落盘再调用
+  `next()`,保证"执行前写入",且不会被下游监听器短路掉。
+- 会话项目目录取自 `agent.session.header.cwd`(durable header 中的已验证
+  绝对路径);写入走 `ctx.fs`(`resolve` → `stat`/`readText` → `writeText`),
+  因此沙箱/远程 fs 部署下日志会跟随该部署的文件系统视图。
+- **沙箱策略**:写盘的 per-call policy 用 `ctx.sandboxPolicy.resolve({ session })`
+  从**会话**解析——会话 cwd 就是它的 workspace-write 边界(与工具层一致)。
+  若不传,沙箱 fs 会退回部署默认根(dsh 的启动目录),导致"从 A 目录启动、
+  项目在 B 目录"时写入被 `FS_SANDBOX_DENIED` 拒绝。会话被设为 read-only 时,
+  日志写入同样被拒(尊重会话策略),dsh 终端会有 warn 提示。
+- **追加规则(分隔线永不重复)**:写入前检查日志文件结尾,分三种情况——
+  空文件直接写条目;结尾已是分隔线(如手工补的 `---`,含旧式无空行写法)
+  则只补分隔线的"后半段"(空行)再接新条目;结尾是普通条目则插入完整
+  `
+
+---
+
+`。因此手工维护与插件写入可无缝混排。已知歧义:若上一条
+  需求自身恰以 `---` 行结尾,会被当作分隔线(极罕见,可手工修文件)。
+- 同一目标的读-改-写按 `targetKey` 串行化(进程内 promise 链),多会话并发
+  追加同一文件不会交错覆盖。
+- 所有 `ctx.on` / `ctx.effect` 注册随插件卸载自动撤销(可逆副作用)。
+
+## 调试
+
+排查期插件会向 `~/.dsh/prompt-journal-debug.log` 追加每步痕迹(模块加载、
+apply、pre-step、append ok/FAILED),每条需求仅数行,稳定后可移除。

+ 256 - 0
prompt-journal/behavior-check.mjs

@@ -0,0 +1,256 @@
+/**
+ * Standalone behavior check for prompt-journal/index.ts.
+ * Runs on Node >= 22.18 (type stripping); no build step.
+ *
+ * Fakes: Context (logger/get/on/effect), FileSystem (node:fs in a temp dir),
+ * Agent/Session/UserMessage shapes used by the listener.
+ */
+import assert from 'node:assert/strict'
+import { mkdtempSync, readFileSync, statSync, writeFileSync, rmSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+
+const plugin = await import('./index.ts')
+
+// ---- fake fs provider backed by node:fs ----------------------------------
+const cwd = mkdtempSync(join(tmpdir(), 'prompt-journal-'))
+let resolves = 0
+let lastWritePolicy = undefined
+const fakePolicy = {
+  resolve: (req) => ({ mode: 'workspace-write', workspaceRoot: req.session.header.cwd }),
+}
+const fakeFs = {
+  resolve: async (path, opts) => {
+    resolves += 1
+    assert.ok(opts?.cwd === cwd, 'resolve must receive the session cwd')
+    assert.ok(opts.signal instanceof AbortSignal)
+    const abs = join(cwd, path)
+    return { targetKey: abs, displayPath: abs }
+  },
+  stat: async (t) => {
+    try {
+      const s = statSync(t.targetKey)
+      return s.isFile() ? { type: 'file' } : { type: 'directory' }
+    } catch (error) {
+      if (error.code === 'ENOENT') return undefined
+      throw error
+    }
+  },
+  readText: async (t) => readFileSync(t.targetKey, 'utf8'),
+  writeText: async (t, content, _expected, _signal, policy) => {
+    lastWritePolicy = policy
+    writeFileSync(t.targetKey, content)
+    return { version: 'v1' }
+  },
+}
+
+// ---- fake ctx --------------------------------------------------------------
+const listeners = []
+const effects = []
+const warnings = []
+const fakeCtx = {
+  logger: () => ({ warn: (m) => warnings.push(m) }),
+  get: (key) => (key === 'fs' ? fakeFs : key === 'sandboxPolicy' ? fakePolicy : undefined),
+  on: (name, fn, opts) => { listeners.push({ name, fn, opts }) },
+  effect: (fn) => { effects.push(fn) },
+}
+
+plugin.apply(fakeCtx, { file: 'prompts.md', separator: '\n\n---\n\n' })
+assert.equal(listeners.length, 1, 'registers exactly one listener')
+assert.equal(listeners[0].name, 'agent/pre-step')
+assert.deepEqual(listeners[0].opts, { prepend: true }, 'listener is prepended')
+
+const preStep = listeners[0].fn
+const signal = new AbortController().signal
+const userMsg = (text, source = { kind: 'user' }) => ({
+  id: 'm1', role: 'user', source,
+  content: text === undefined ? [] : [{ type: 'text', text }],
+})
+const agent = (origin, cwdOverride) => ({
+  session: { header: { origin, cwd: cwdOverride ?? cwd } },
+})
+let delegated = 0
+const next = async () => { delegated += 1; return { kind: 'enter', messages: [] } }
+
+// 1. user prompt on a root agent is journaled before delegation
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('第一条需求')], turn: 1, step: 1, signal },
+  next,
+)
+assert.equal(delegated, 1)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '第一条需求\n',
+  'first entry is plain text + newline',
+)
+
+// 2. second entry separated by \n\n---\n\n
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('第二条需求')], turn: 2, step: 1, signal },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '第一条需求\n\n---\n\n第二条需求\n',
+)
+
+// 3. plugin-source and subagent messages are not journaled
+await preStep(
+  {
+    agent: agent('subagent'),
+    messages: [userMsg('子任务'), userMsg('也是子任务', { kind: 'plugin', plugin: 'x' })],
+    turn: 3, step: 1, signal,
+  },
+  next,
+)
+await preStep(
+  {
+    agent: agent(undefined),
+    messages: [userMsg('上下文注入', { kind: 'plugin', plugin: 'time-context' })],
+    turn: 4, step: 1, signal,
+  },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '第一条需求\n\n---\n\n第二条需求\n',
+  'subagent/plugin messages are skipped',
+)
+
+// 4. whitespace-only text, image-only messages, empty batch are skipped
+await preStep(
+  {
+    agent: agent(undefined),
+    messages: [
+      userMsg('   '),
+      { id: 'm2', role: 'user', source: { kind: 'user' }, content: [{ type: 'image', attachment: {} }] },
+    ],
+    turn: 5, step: 1, signal,
+  },
+  next,
+)
+await preStep({ agent: agent(undefined), messages: [], turn: 6, step: 2, signal }, next)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '第一条需求\n\n---\n\n第二条需求\n',
+  'empty/image-only messages are skipped',
+)
+
+// 5. pre-existing file (with CRLF and trailing blanks) is appended, not rewritten
+writeFileSync(join(cwd, 'prompts.md'), '历史条目。\r\n\r\n')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('新条目')], turn: 7, step: 1, signal },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '历史条目。\n\n---\n\n新条目\n',
+  'trailing blanks trimmed, separator inserted',
+)
+
+// 6. multi-block text joined with newline; multiple messages -> multiple entries
+await preStep(
+  {
+    agent: agent(undefined),
+    messages: [userMsg('多行\r\n第二行'), userMsg('独立需求')],
+    turn: 8, step: 1, signal,
+  },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '历史条目。\n\n---\n\n新条目\n\n---\n\n多行\r\n第二行\n\n---\n\n独立需求\n',
+)
+
+// 7. write failure is contained: warn once, step still delegated
+const failingFs = { ...fakeFs, writeText: async () => { throw new Error('disk full') } }
+let getCalls = 0
+fakeCtx.get = (key) => { getCalls += 1; return key === 'fs' ? failingFs : key === 'sandboxPolicy' ? fakePolicy : undefined }
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('会失败')], turn: 9, step: 1, signal },
+  next,
+)
+assert.equal(delegated, 9)
+assert.equal(warnings.length, 1)
+assert.match(warnings[0], /failed to journal a prompt: disk full/)
+
+// 8. providerless deployment: one warning, delegation continues
+fakeCtx.get = () => undefined
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('无 fs')], turn: 10, step: 1, signal },
+  next,
+)
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('无 fs 二次')], turn: 11, step: 1, signal },
+  next,
+)
+assert.equal(warnings.length, 2)
+assert.match(warnings[1], /no fs provider/)
+
+// 9. aborted signal skips journaling but still delegates
+fakeCtx.get = (key) => (key === 'fs' ? fakeFs : key === 'sandboxPolicy' ? fakePolicy : undefined)
+const aborted = AbortSignal.abort('stop')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('已取消')], turn: 12, step: 1, signal: aborted },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '历史条目。\n\n---\n\n新条目\n\n---\n\n多行\r\n第二行\n\n---\n\n独立需求\n',
+)
+
+// 9b. file already ending on the separator rule line: only the tail is added
+writeFileSync(join(cwd, 'prompts.md'), '历史条目。\n\n---\n\n')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('接在分隔线后')], turn: 13, step: 1, signal },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '历史条目。\n\n---\n\n接在分隔线后\n',
+  'trailing rule line is not duplicated',
+)
+
+// 9c. old-style rule ending (no blank lines) also recognized
+writeFileSync(join(cwd, 'prompts.md'), '旧式结尾\n---\n')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('旧式追加')], turn: 14, step: 1, signal },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  '旧式结尾\n---\n\n旧式追加\n',
+  'old-style trailing rule gets only the blank-line half',
+)
+
+// 9d. trailing blank lines after the rule are tolerated
+writeFileSync(join(cwd, 'prompts.md'), 'A\n\n---\n\n\n\n')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('B')], turn: 15, step: 1, signal },
+  next,
+)
+assert.equal(
+  readFileSync(join(cwd, 'prompts.md'), 'utf8'),
+  'A\n\n---\n\nB\n',
+  'blank lines after a trailing rule are trimmed',
+)
+
+// 9e. sandbox policy resolves from the session and rides the write
+writeFileSync(join(cwd, 'prompts.md'), '')
+await preStep(
+  { agent: agent(undefined), messages: [userMsg('沙箱验证')], turn: 16, step: 1, signal },
+  next,
+)
+assert.deepEqual(lastWritePolicy, { mode: 'workspace-write', workspaceRoot: cwd })
+
+// 10. effect disposer clears serialization state
+assert.equal(effects.length, 1)
+effects[0]()
+assert.equal(resolves > 0, true)
+
+// 11. config validation fails loudly
+assert.throws(() => plugin.apply(fakeCtx, { file: '  ', separator: '\n\n---\n\n' }), /non-empty/)
+assert.throws(() => plugin.apply(fakeCtx, { file: 'a.md', separator: ' ' }), /non-empty/)
+
+rmSync(cwd, { recursive: true, force: true })
+console.log('prompt-journal behavior checks: all passed')

+ 8 - 0
prompt-journal/cordis.patch.yml

@@ -0,0 +1,8 @@
+# 组合包配置层:把 prompt-journal 插件插入 profile。
+# 插件行按包名引用(bundle 安装时由 Node 模块解析找到代码)。
+- insert:
+    - id: prompt-journal
+      name: dsh-prompt-journal
+      # config:
+      #   file: prompts.md      # 日志文件,相对会话 cwd(也可绝对路径)
+      #   separator: "\n\n---\n\n"  # 条目之间的分隔符

+ 6 - 0
prompt-journal/dev.overlay.yml

@@ -0,0 +1,6 @@
+# 本地开发 overlay:直接以源码加载 prompt-journal(无需安装)。
+# 用法:在任意目录执行  dsh web --patch 'D:/projects/dsh-plugins/prompt-journal/dev.overlay.yml'
+# 注意:Windows 下插件路径必须是 file:// URL(ESM 限制)。
+- insert:
+    - id: prompt-journal
+      name: 'file:///D:/projects/dsh-plugins/prompt-journal/index.ts'

+ 156 - 0
prompt-journal/index.ts

@@ -0,0 +1,156 @@
+/**
+ * Prompt journal: append every user-sent prompt to the session project's
+ * journal file (`prompts.md` by default) before the step executes.
+ *
+ * The listener occupies the head of the `agent/pre-step` waterfall, journals
+ * the claimed user messages, then delegates unchanged. Subagent sessions and
+ * non-user sources (plugin context, tool results) are not journaled. Writes
+ * go through the optional `ctx.fs` provider, so providerless products mount
+ * this plugin as a no-op.
+ *
+ * @module dsh-prompt-journal
+ */
+
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
+import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs'
+import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
+import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
+
+/** Cordis plugin name used by loader diagnostics. */
+export const name = 'prompt-journal'
+
+/** The agent registry that owns pre-step dispatch scoping. */
+export const inject = ['agents']
+
+/** Journal target file and entry framing. Invalid values fail plugin load. */
+export interface Config {
+  /** Journal file, relative to the session cwd (or absolute). */
+  file: string
+  /** Separator inserted between two journal entries. */
+  separator: string
+}
+
+/** Schemastery validation for {@link Config}. */
+export const Config: Schema<Config> = Schema.object({
+  file: Schema.string().default('prompts.md'),
+  separator: Schema.string().default('\n\n---\n\n'),
+})
+
+/** Extract the visible prompt text of one message, or `undefined` when empty. */
+function promptText(message: UserMessage): string | undefined {
+  const texts: string[] = []
+  for (const block of message.content) {
+    if (block.type === 'text' && block.text.trim().length > 0) texts.push(block.text)
+  }
+  const text = texts.join('\n').trim()
+  return text.length === 0 ? undefined : text
+}
+
+/** Whether one agent's session belongs to the interactive user, not a delegation. */
+function isRootAgent(agent: Agent): boolean {
+  return agent.session.header.origin !== 'subagent'
+}
+
+/**
+ * Register the journaling pre-step listener for the lifetime of `ctx`.
+ * @param ctx - plugin context; the listener is disposed with it.
+ * @param config - journal target file and entry framing.
+ * @throws when the configured file or separator is blank.
+ */
+export function apply(ctx: Context, config: Config): void {
+  const file = config.file
+  const separator = config.separator
+  if (file.trim().length === 0) {
+    throw new TypeError(`prompt-journal: file must be a non-empty string, got ${JSON.stringify(file)}`)
+  }
+  if (separator.trim().length === 0) {
+    throw new TypeError(`prompt-journal: separator must be a non-empty string, got ${JSON.stringify(separator)}`)
+  }
+  const logger = ctx.logger('prompt-journal')
+  let warnedProviderless = false
+
+  /** Read-modify-write chains keyed by journal target; unloading the plugin clears them. */
+  const journalTails = new Map<string, Promise<void>>()
+  ctx.effect(() => () => journalTails.clear())
+
+  /**
+   * Append one entry through a per-target serialized read-modify-write.
+   * @param fs - filesystem provider backing the journal.
+   * @param session - the owning session; its cwd is the journal's base and the
+   * sandbox workspace boundary, and its logged mode rides the write policy.
+   * @param entry - trimmed prompt text to append.
+   * @param signal - the owning step's cancellation signal.
+   */
+  const appendEntry = async (
+    fs: FileSystem,
+    session: Session,
+    entry: string,
+    signal: AbortSignal,
+  ): Promise<void> => {
+    const target: FsTarget = await fs.resolve(
+      file,
+      session.header.cwd === undefined ? { signal } : { cwd: session.header.cwd, signal },
+    )
+    const previous = journalTails.get(target.targetKey) ?? Promise.resolve()
+    const task = previous.then(async () => {
+      const existing = await fs.stat(target, signal)
+      const priorText = existing === undefined ? '' : (await fs.readText(target, signal)).trimEnd()
+      // Three end states decide the join, so the rule line is never duplicated:
+      // an empty file writes the bare entry; a file already ending on the
+      // separator's rule line (e.g. a hand-added trailing '---') gets only the
+      // separator's tail (the blank-line half); anything else gets the full
+      // separator between the previous entry and this one.
+      const rule = separator.trim()
+      const lastLine = priorText.length === 0 ? '' : priorText.split(/\r?\n/).at(-1) ?? ''
+      const endsOnRule = priorText.length > 0 && rule.length > 0 && lastLine.trim() === rule
+      const join = priorText.length === 0
+        ? ''
+        : endsOnRule
+          ? separator.slice(separator.lastIndexOf(rule) + rule.length)
+          : separator
+      const content = `${priorText}${join}${entry}\n`
+      // Resolve the per-call sandbox policy from the SESSION, mirroring the
+      // tool layer: a session's cwd is its workspace-write boundary. Without
+      // this, a sandboxing fs provider falls back to the deployment root (the
+      // process launch directory) and denies journaling a different project.
+      const policyService: SandboxPolicyService | undefined = ctx.get('sandboxPolicy')
+      const sandboxPolicy = policyService === undefined
+        ? undefined
+        : policyService.resolve({ session })
+      await fs.writeText(target, content, undefined, signal, sandboxPolicy)
+    })
+    // Keep the serialization chain alive even when this append fails.
+    journalTails.set(target.targetKey, task.then(() => {}, () => {}))
+    await task
+  }
+
+  ctx.on('agent/pre-step', async (
+    { agent, messages, signal },
+    next,
+  ): Promise<PreStepDecision> => {
+    const fs = ctx.get('fs')
+    if (fs === undefined) {
+      if (!warnedProviderless) {
+        warnedProviderless = true
+        logger.warn('no fs provider is registered; prompts are not journaled')
+      }
+      return next()
+    }
+    if (isRootAgent(agent) && !signal.aborted) {
+      for (const message of messages) {
+        if (message.source.kind !== 'user') continue
+        const entry = promptText(message)
+        if (entry === undefined) continue
+        try {
+          await appendEntry(fs, agent.session, entry, signal)
+        } catch (error: unknown) {
+          // Journaling observes the step; it must never fail or delay recovery.
+          logger.warn(`failed to journal a prompt: ${error instanceof Error ? error.message : String(error)}`)
+        }
+      }
+    }
+    return next()
+  }, { prepend: true })
+}

+ 28 - 0
prompt-journal/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "dsh-prompt-journal",
+  "description": "Append every user-sent prompt to the session project's prompts.md before execution",
+  "version": "0.1.0",
+  "type": "module",
+  "main": "index.ts",
+  "files": [
+    "index.ts",
+    "cordis.patch.yml",
+    "README.md"
+  ],
+  "dsh": {
+    "bundle": {
+      "patch": "./cordis.patch.yml"
+    }
+  },
+  "dependencies": {
+    "@deepseek-ai/schemastery": "^3.18.1"
+  },
+  "devDependencies": {
+    "@deepseek-ai/cordis": "4.0.1",
+    "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-fs": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
+    "@deepseek-ai/dsh-session": "0.1.1-rc.2"
+  },
+  "license": "MIT"
+}

+ 50 - 0
prompts.md

@@ -0,0 +1,50 @@
+克隆 deepseek-harness 项目到项目路径里,然后创建 .gitignore ,忽略该项目。
+然后探索该项目,了解插件的创作机制,写入 docs。
+
+---
+
+有点过于技术化了,偏向实现细节。能不能从概念的角度再写一篇md文档,从科普的角度,由浅入深讲一下?
+
+---
+
+帮我看看,为什么我的dsh在发送消息时提示:AbortSignal.any is not a function?查看我的node版本,给出答案。
+
+---
+
+查看 docs 下的 plugin 编写说明,然后开始写一个plugin:我打算将在dsh中发送的所有需求,执行前写入对应项目的 prompts.md 文件中,需求之间以“\n---\n”分隔。该plugin新建一个子目录编写,你自己为plugin命名。
+
+---
+
+以“\n\n---\n\n”分隔吧。
+
+---
+
+你有没有考虑多种情况?如现在prompts.md结尾已经是“---”了,那么只需要插入一个空行(\n\n),再插入我的需求即可;如果现在promtps.md结尾是上次需求,那么需要插入一个空行,一行“---”,一个空行,再插入我的需求。
+
+---
+
+此条为测试消息。
+
+---
+
+补充完善 .gitignore,将该技术栈下常见的要忽略文件写入。
+
+---
+
+忽略.spec-workflow/。然后执行 git init
+
+---
+
+你是否可以使用skill?如果可以的话,读取路径顺序是什么?
+
+---
+
+新建 ~/.agents/skills 目录,将 ~/.claude/skills 目录下的技能复制过去。
+
+---
+
+使用技能 git-commit-log-gen 生成提交日志,可以参考 prompts.md 中的需求。
+
+---
+
+使用该日志,git add 并 commit 吧。

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "nodenext",
+    "moduleResolution": "nodenext",
+    "lib": ["esnext"],
+    "types": ["node"],
+    "strict": true,
+    "noEmit": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["prompt-journal/index.ts"]
+}