fluffy-context 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -136
- package/dist/src/agent/api.d.ts +8 -0
- package/dist/src/agent/api.js +285 -0
- package/dist/src/agent/index.d.ts +3 -0
- package/dist/src/agent/index.js +1 -0
- package/dist/src/agent/types.d.ts +50 -0
- package/dist/src/agent/types.js +1 -0
- package/dist/src/capture/context-filter.d.ts +7 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +382 -30
- package/dist/src/git/git-adapter.d.ts +2 -0
- package/dist/src/hooks/claude-code.d.ts +1 -0
- package/dist/src/hooks/claude-code.js +84 -0
- package/dist/src/integrations/claude-code.d.ts +19 -0
- package/dist/src/integrations/claude-code.js +122 -0
- package/dist/src/mcp/main.d.ts +1 -0
- package/dist/src/mcp/main.js +5 -0
- package/dist/src/mcp/server.d.ts +2 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/project/project-resolver.d.ts +1 -0
- package/dist/src/runtime/diagnostics.d.ts +6 -0
- package/dist/src/runtime/diagnostics.js +34 -5
- package/dist/src/runtime/init.d.ts +7 -0
- package/dist/src/runtime/knowledge.d.ts +9 -0
- package/dist/src/runtime/knowledge.js +179 -12
- package/dist/src/runtime/notes.d.ts +6 -0
- package/dist/src/runtime/notes.js +173 -0
- package/dist/src/runtime/runtime.d.ts +11 -0
- package/dist/src/runtime/runtime.js +48 -18
- package/dist/src/runtime/types.d.ts +294 -0
- package/dist/src/storage/atomic-write.d.ts +1 -0
- package/dist/src/storage/json-store.d.ts +6 -0
- package/dist/src/storage/layout.d.ts +13 -0
- package/dist/src/storage/layout.js +3 -0
- package/dist/src/storage/lock.d.ts +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +1 -0
- package/package.json +48 -11
- package/skills/fluffy-context/SKILL.md +345 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fluffy_CX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
# Context Runtime
|
|
2
2
|
|
|
3
|
-
Context Runtime
|
|
3
|
+
`fluffy-context` 是面向 AI 编程会话的本地 Context Runtime CLI。0.3.0 提供可重复、安全调用的任务定位入口、MCP 工具,以及 Claude Code 的阶段性工作流引导,让 Agent 不必自行拼接恢复、共识发现与短期记录。
|
|
4
4
|
|
|
5
|
-
它更接近“Context 的 Git”,而不是代码备份工具:Git
|
|
5
|
+
它更接近“Context 的 Git”,而不是代码备份工具:Git 仍然负责代码和真实文件变更;Context Runtime 负责 AI 工作状态、已验证共识与交接信息的本地保存和恢复。
|
|
6
6
|
|
|
7
7
|
## 适用场景
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
- 下班前保存当前任务状态,下一次会话继续工作。
|
|
10
|
+
- 以有界摘要恢复进行中的任务,降低重复阅读和 Token 消耗。
|
|
11
|
+
- 让 Agent 在开始任务时获得已验证 Knowledge、匹配 Deadend 与未吸收 Note。
|
|
12
|
+
- 通过 MCP 或 Claude Code Hooks 降低 Agent 主动调用 Context 工具的决策成本。
|
|
13
|
+
- 记录完成项、待办、阻塞、决策、风险与已验证不可行的方案。
|
|
14
14
|
|
|
15
15
|
## 环境要求
|
|
16
16
|
|
|
17
17
|
- Node.js `>=20.19.0`
|
|
18
|
-
- Git
|
|
18
|
+
- Git 可选。项目在 Git 仓库内时,CLI 会记录分支与 commit。
|
|
19
19
|
|
|
20
20
|
## 安装
|
|
21
21
|
|
|
22
|
-
发布后可以使用 npm 全局安装:
|
|
23
|
-
|
|
24
22
|
```bash
|
|
25
23
|
npm install --global fluffy-context
|
|
24
|
+
ctx --help
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
也可以在项目目录中使用:
|
|
29
28
|
|
|
30
29
|
```bash
|
|
31
30
|
npx fluffy-context --help
|
|
@@ -33,26 +32,13 @@ npx fluffy-context --help
|
|
|
33
32
|
|
|
34
33
|
## 快速开始
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
在项目根目录初始化:
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
38
|
ctx init
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```text
|
|
45
|
-
.context/
|
|
46
|
-
├── manifest.json
|
|
47
|
-
├── index.json
|
|
48
|
-
├── contexts/
|
|
49
|
-
├── locks/
|
|
50
|
-
├── knowledge.json # 首次使用 learn 后创建
|
|
51
|
-
└── deadends.json # 首次使用 deadend 后创建
|
|
52
|
-
.contextignored
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
保存一次工作状态:
|
|
41
|
+
这会创建本地 `.context/` 存储和 `.contextignored` 规则文件。保存阶段性工作:
|
|
56
42
|
|
|
57
43
|
```bash
|
|
58
44
|
ctx checkpoint \
|
|
@@ -62,181 +48,145 @@ ctx checkpoint \
|
|
|
62
48
|
--pending "补充异常路径测试,运行集成测试" \
|
|
63
49
|
--decisions "订单状态由服务端状态机统一维护" \
|
|
64
50
|
--risks "第三方回调可能重复到达" \
|
|
65
|
-
--files "src/order/state-machine.ts,src/order
|
|
51
|
+
--files "src/order/state-machine.ts,src/order-state-machine.test.ts"
|
|
66
52
|
```
|
|
67
53
|
|
|
68
|
-
|
|
54
|
+
新会话中,Agent 或宿主优先使用:
|
|
69
55
|
|
|
70
56
|
```bash
|
|
71
|
-
ctx
|
|
57
|
+
ctx orient "订单状态机异常路径" --max-chars 4000
|
|
72
58
|
```
|
|
73
59
|
|
|
74
|
-
`
|
|
60
|
+
`ctx orient` 只读地聚合当前 Context 摘要、与查询匹配的**已验证** Knowledge 和 Deadend,以及当前 Context 尚未吸收的 Note。它不更新 `lastUsedAt`、不创建 Snapshot,也不会记录 usage 噪声。省略查询时可只恢复当前任务和 open Notes:
|
|
75
61
|
|
|
76
62
|
```bash
|
|
77
|
-
ctx
|
|
63
|
+
ctx orient
|
|
64
|
+
ctx orient --context <context-id> --note-limit 10
|
|
78
65
|
```
|
|
79
66
|
|
|
80
|
-
##
|
|
81
|
-
|
|
82
|
-
### `ctx init`
|
|
67
|
+
## Agent 工作流
|
|
83
68
|
|
|
84
|
-
|
|
69
|
+
推荐阶段:
|
|
85
70
|
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
ctx init path/to/project
|
|
89
|
-
ctx init --path path/to/project
|
|
71
|
+
```text
|
|
72
|
+
Orient → Plan → Implement → Verify → Handoff
|
|
90
73
|
```
|
|
91
74
|
|
|
92
|
-
|
|
75
|
+
1. **Orient**:调用 `ctx orient` 或 MCP `context_orient`,读取有界任务上下文。
|
|
76
|
+
2. **Plan**:在广泛改动前先形成可执行计划。
|
|
77
|
+
3. **Implement**:用 `ctx note add` 记录有价值的观察、决策、阻塞或行动;不要为每条记录创建 Snapshot。
|
|
78
|
+
4. **Verify**:执行适用的构建和测试,并把有意义的失败记录为 Note。
|
|
79
|
+
5. **Handoff**:任务阶段结束时显式运行一次 `ctx checkpoint`,记录完成项、待办、决策和风险;Hook 不会自动 checkpoint。
|
|
80
|
+
|
|
81
|
+
## 常用命令
|
|
93
82
|
|
|
94
|
-
|
|
83
|
+
业务命令默认输出格式化 JSON;帮助输出为纯文本。错误写入标准错误并返回非零退出码。`ctx --version` 输出一个 JSON 字符串。
|
|
95
84
|
|
|
96
85
|
```bash
|
|
97
|
-
ctx
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
ctx init
|
|
87
|
+
ctx checkpoint --title "修复支付回调" --progress "已定位签名校验失败原因"
|
|
88
|
+
ctx resume --max-chars 2000
|
|
89
|
+
ctx orient "支付回调" --knowledge-limit 5 --deadend-limit 3
|
|
90
|
+
ctx note add "测试环境缺少回调凭据" --kind problem --context <context-id>
|
|
91
|
+
ctx activity --context <context-id> --open
|
|
92
|
+
ctx knowledge discover "支付回调"
|
|
93
|
+
ctx knowledge verify <knowledge-id>
|
|
94
|
+
ctx deadend verify <deadend-id>
|
|
95
|
+
ctx status
|
|
96
|
+
ctx doctor
|
|
107
97
|
```
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
### `ctx checkpoint` 和 `ctx resume`
|
|
110
100
|
|
|
111
|
-
|
|
101
|
+
第一次 checkpoint 创建 baseline,后续变化创建 patch。相同内容返回 `no_change`;短时间内的变化可能返回 `rate_limited`,应在完成更多阶段工作后再保存。`ctx resume` 返回轻量摘要和按需使用的完整 details,并会保留其既有的 `lastUsedAt` 更新语义。
|
|
112
102
|
|
|
113
|
-
|
|
103
|
+
### `ctx note` 和 `ctx activity`
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
ctx resume
|
|
117
|
-
ctx resume --context <context-id>
|
|
118
|
-
ctx resume --path path/to/project --max-chars 4000
|
|
119
|
-
```
|
|
105
|
+
`ctx note add` 是开发过程中的低成本记录入口,不创建 Snapshot。阶段性 checkpoint 可通过 `--absorb-notes` 吸收当前 Context 的 open Note。`ctx activity` 提供只读的 Note 与 Snapshot 时间线,适合人类查看 Agent 的进度。
|
|
120
106
|
|
|
121
|
-
###
|
|
107
|
+
### Knowledge 与 Deadend
|
|
122
108
|
|
|
123
|
-
|
|
109
|
+
`ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all` 审查。
|
|
124
110
|
|
|
125
111
|
```bash
|
|
126
|
-
ctx
|
|
112
|
+
ctx learn "订单取消后不能再次进入支付中状态" --scope project
|
|
113
|
+
ctx deadend --attempt "使用共享可变单例保存订单状态" --reason "并发测试出现跨用例状态泄漏"
|
|
114
|
+
ctx knowledge discover "订单取消"
|
|
127
115
|
```
|
|
128
116
|
|
|
129
|
-
|
|
117
|
+
## MCP 集成
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
`ctx agent serve` 启动一个 MCP stdio server,暴露一个工具:`context_orient`。该 server 的标准输入和输出均属于 MCP 协议,不能在交互式终端直接使用或混入日志。
|
|
132
120
|
|
|
133
121
|
```bash
|
|
134
|
-
ctx
|
|
122
|
+
ctx agent serve
|
|
135
123
|
```
|
|
136
124
|
|
|
137
|
-
|
|
125
|
+
`context_orient` 接受可选的 `path`、`contextId`、`query`、`scope`、`maxChars`、`knowledgeLimit`、`deadendLimit` 和 `noteLimit`。它的结果与 `ctx orient` 相同:`ready` 是成功的任务定位结果,初始化但没有可恢复 Context 时返回成功的 `no_context`。输入验证或 Runtime 错误为单次工具错误,不会终止 server。
|
|
126
|
+
|
|
127
|
+
## Claude Code 集成
|
|
138
128
|
|
|
139
|
-
|
|
129
|
+
先查看项目是否需要安装集成:
|
|
140
130
|
|
|
141
131
|
```bash
|
|
142
|
-
ctx
|
|
143
|
-
|
|
144
|
-
--context <context-id> \
|
|
145
|
-
--snapshot <snapshot-id> \
|
|
146
|
-
--evidence "src/order/state-machine.ts,订单服务接口约束"
|
|
132
|
+
ctx integrate claude inspect
|
|
133
|
+
ctx integrate claude install
|
|
147
134
|
```
|
|
148
135
|
|
|
149
|
-
|
|
136
|
+
`install` 默认只输出预览,不创建或修改文件。确认后才显式写入:
|
|
150
137
|
|
|
151
138
|
```bash
|
|
152
|
-
ctx
|
|
153
|
-
ctx knowledge --all
|
|
154
|
-
ctx knowledge verify <knowledge-id>
|
|
139
|
+
ctx integrate claude install --apply
|
|
155
140
|
```
|
|
156
141
|
|
|
157
|
-
|
|
142
|
+
安装会以幂等方式合并项目本地配置:
|
|
158
143
|
|
|
159
|
-
|
|
144
|
+
- `.claude/settings.json`:`SessionStart` 与 `UserPromptSubmit` Hook,分别调用 `ctx hook claude-code session-start` 和 `ctx hook claude-code user-prompt`。
|
|
145
|
+
- `.mcp.json`:名为 `fluffy-context` 的 stdio MCP server,调用 `ctx agent serve`。
|
|
160
146
|
|
|
161
|
-
|
|
147
|
+
安装会保留无关的 Hook、权限和 MCP server;遇到无效 JSON、无效相关结构或同名冲突配置会拒绝覆盖。Hook 仅注入有界的 Orient/Plan/Implement/Verify/Handoff 指引,输入损坏、项目未初始化或查询失败时会 fail open,不阻塞 Claude Code 任务,也不会自动创建 checkpoint。
|
|
162
148
|
|
|
163
|
-
|
|
164
|
-
ctx deadend \
|
|
165
|
-
--attempt "使用共享可变单例保存订单状态" \
|
|
166
|
-
--reason "并发测试出现跨用例状态泄漏" \
|
|
167
|
-
--scope project \
|
|
168
|
-
--context <context-id> \
|
|
169
|
-
--evidence "test/order-state.test.ts"
|
|
170
|
-
```
|
|
149
|
+
## TypeScript Agent API
|
|
171
150
|
|
|
172
|
-
|
|
151
|
+
使用公开包入口,不要导入内部 `dist/...` 文件:
|
|
173
152
|
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
153
|
+
```ts
|
|
154
|
+
import { contextOrient, loadContext, saveContext, searchContext } from 'fluffy-context';
|
|
155
|
+
|
|
156
|
+
const orientation = await contextOrient(process.cwd(), {
|
|
157
|
+
query: 'payment callback',
|
|
158
|
+
maxChars: 2400,
|
|
159
|
+
});
|
|
178
160
|
```
|
|
179
161
|
|
|
180
|
-
|
|
162
|
+
`fluffy-context/agent` 提供相同的 Agent API。公开类型声明随包发布。Runtime 的存储布局与内部模块不是兼容性承诺。
|
|
163
|
+
|
|
164
|
+
## `.contextignored` 与安全边界
|
|
181
165
|
|
|
182
|
-
`
|
|
166
|
+
`.contextignored` 语法接近 `.gitignore`,用于排除不应作为关联文件保存的项目路径:
|
|
183
167
|
|
|
184
168
|
```gitignore
|
|
185
|
-
# 私有目录
|
|
186
169
|
private/
|
|
187
|
-
|
|
188
|
-
# 本地生成文件
|
|
189
170
|
*.generated.ts
|
|
190
|
-
|
|
191
|
-
# 不纳入上下文的临时记录
|
|
192
171
|
notes/draft-*
|
|
193
172
|
```
|
|
194
173
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
## 存储与安全边界
|
|
198
|
-
|
|
199
|
-
- Snapshot 按 baseline/patch 保存,不会每次复制整个项目文件树。
|
|
200
|
-
- `index.json` 是可重建索引,不是唯一业务数据来源。
|
|
201
|
-
- Context、Knowledge、Deadend 使用独立文件保存。
|
|
202
|
-
- 写入使用临时文件替换,并通过项目级锁避免并发覆盖。
|
|
203
|
-
- 默认只恢复摘要字段;完整结构化内容位于 `details` 中按需读取。
|
|
204
|
-
- CLI 不会读取或保存被内置保护规则排除的文件内容。
|
|
205
|
-
|
|
206
|
-
## 命令输出
|
|
174
|
+
内置保护规则优先,不能被否定规则绕过:`.context/`、`.git/`、依赖和构建目录、`.env` 文件、密钥、常见 credential/token 文件、绝对路径和 `..` 越界路径都不会被作为 Context 关联文件保存。
|
|
207
175
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
ctx --help
|
|
212
|
-
ctx --version
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
错误信息写入标准错误,并以非零退出码结束。
|
|
216
|
-
|
|
217
|
-
## 开发
|
|
218
|
-
|
|
219
|
-
安装依赖并运行测试:
|
|
176
|
+
## 开发与发布验证
|
|
220
177
|
|
|
221
178
|
```bash
|
|
222
179
|
npm install
|
|
223
|
-
npm test
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
只构建 TypeScript:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
180
|
npm run build
|
|
181
|
+
npm test
|
|
182
|
+
npm run pack:check
|
|
183
|
+
npm pack --dry-run --json
|
|
230
184
|
```
|
|
231
185
|
|
|
232
|
-
|
|
186
|
+
运行时依赖 `@modelcontextprotocol/server` 与 `zod` 以提供 MCP 适配;核心 Context 存储仍然保持本地优先。`prepack` 会重新构建发布产物,`prepublishOnly` 在未来手动发布前运行测试;这些命令不会发布 npm 包。
|
|
233
187
|
|
|
234
188
|
## 当前范围
|
|
235
189
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
```text
|
|
239
|
-
init → checkpoint → resume
|
|
240
|
-
```
|
|
190
|
+
0.3.0 聚焦单项目、本地优先的可靠闭环和 Agent 采用路径:有界 Orient、显式 checkpoint、确定性 Knowledge/Deadend discovery、MCP `context_orient` 与可选的 Claude Code Hook 集成。
|
|
241
191
|
|
|
242
|
-
|
|
192
|
+
远程同步、多人协作、复杂语义检索、自动模型总结、自动 checkpoint、Context merge,以及更多状态变更型 MCP 工具不属于当前版本范围。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
|
|
2
|
+
import type { ContextOrientOptions, ContextOrientResult } from './types.js';
|
|
3
|
+
export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
|
|
4
|
+
minSaveIntervalMs?: number;
|
|
5
|
+
}): Promise<AgentSaveResult>;
|
|
6
|
+
export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions): Promise<AgentLoadResult>;
|
|
7
|
+
export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions): Promise<ContextOrientResult>;
|
|
8
|
+
export declare function searchContext(startPath: string | undefined, query: string, options?: ContextSearchOptions): Promise<ContextSearchResult>;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
3
|
+
import { contextsRoot, contextMetadataPath } from '../storage/layout.js';
|
|
4
|
+
import { isRecord, readJson } from '../storage/json-store.js';
|
|
5
|
+
import { checkpoint, rebuildSnapshot, resume } from '../runtime/runtime.js';
|
|
6
|
+
import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
|
|
7
|
+
import { listNotes } from '../runtime/notes.js';
|
|
8
|
+
function normalized(value) {
|
|
9
|
+
return value.trim().toLocaleLowerCase();
|
|
10
|
+
}
|
|
11
|
+
function terms(query) {
|
|
12
|
+
return [...new Set(normalized(query).split(/\s+/).filter(Boolean))];
|
|
13
|
+
}
|
|
14
|
+
function limited(value, budget) {
|
|
15
|
+
if (budget.remaining <= 0)
|
|
16
|
+
return '';
|
|
17
|
+
const result = value.length <= budget.remaining ? value : `${value.slice(0, Math.max(0, budget.remaining - 1))}…`;
|
|
18
|
+
budget.remaining -= result.length;
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function limitedList(values, budget) {
|
|
22
|
+
return values.flatMap((value) => {
|
|
23
|
+
if (budget.remaining <= 0)
|
|
24
|
+
return [];
|
|
25
|
+
const result = limited(value, budget);
|
|
26
|
+
return result ? [result] : [];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function textLength(value) {
|
|
30
|
+
return Array.isArray(value) ? value.reduce((total, item) => total + item.length, 0) : value?.length ?? 0;
|
|
31
|
+
}
|
|
32
|
+
function emptyKnowledge(query) {
|
|
33
|
+
return { query, total: 0, truncated: false, hits: [], duplicateIds: [], possibleConflicts: [] };
|
|
34
|
+
}
|
|
35
|
+
function emptyDeadends(query) {
|
|
36
|
+
return { query, total: 0, truncated: false, hits: [] };
|
|
37
|
+
}
|
|
38
|
+
function limitKnowledge(result, budget) {
|
|
39
|
+
let truncated = false;
|
|
40
|
+
const hits = [];
|
|
41
|
+
for (const hit of result.hits) {
|
|
42
|
+
if (budget.remaining <= 0) {
|
|
43
|
+
truncated = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const statement = limited(hit.knowledge.statement, budget);
|
|
47
|
+
const supportingEvidence = limitedList(hit.knowledge.supportingEvidence, budget);
|
|
48
|
+
if (statement.length < hit.knowledge.statement.length || supportingEvidence.length < hit.knowledge.supportingEvidence.length)
|
|
49
|
+
truncated = true;
|
|
50
|
+
hits.push({ ...hit, knowledge: { ...hit.knowledge, statement, supportingEvidence } });
|
|
51
|
+
}
|
|
52
|
+
if (hits.length < result.hits.length)
|
|
53
|
+
truncated = true;
|
|
54
|
+
return { result: { ...result, hits, truncated: result.truncated || truncated }, truncated };
|
|
55
|
+
}
|
|
56
|
+
function limitDeadends(result, budget) {
|
|
57
|
+
let truncated = false;
|
|
58
|
+
const hits = [];
|
|
59
|
+
for (const hit of result.hits) {
|
|
60
|
+
if (budget.remaining <= 0) {
|
|
61
|
+
truncated = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
const attempt = limited(hit.deadend.attempt, budget);
|
|
65
|
+
const reason = limited(hit.deadend.reason, budget);
|
|
66
|
+
const observedEvidence = limitedList(hit.deadend.observedEvidence, budget);
|
|
67
|
+
if (attempt.length < hit.deadend.attempt.length || reason.length < hit.deadend.reason.length || observedEvidence.length < hit.deadend.observedEvidence.length)
|
|
68
|
+
truncated = true;
|
|
69
|
+
hits.push({ ...hit, deadend: { ...hit.deadend, attempt, reason, observedEvidence } });
|
|
70
|
+
}
|
|
71
|
+
if (hits.length < result.hits.length)
|
|
72
|
+
truncated = true;
|
|
73
|
+
return { result: { ...result, hits, truncated: result.truncated || truncated }, truncated };
|
|
74
|
+
}
|
|
75
|
+
function limitNotes(notes, budget) {
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const note of notes) {
|
|
78
|
+
if (budget.remaining <= 0)
|
|
79
|
+
return { notes: result, truncated: true };
|
|
80
|
+
const message = limited(note.message, budget);
|
|
81
|
+
result.push({ ...note, message });
|
|
82
|
+
if (message.length < note.message.length)
|
|
83
|
+
return { notes: result, truncated: true };
|
|
84
|
+
}
|
|
85
|
+
return { notes: result, truncated: false };
|
|
86
|
+
}
|
|
87
|
+
function summary(content, maxChars, branchOrCommitDrift = false) {
|
|
88
|
+
const budget = { remaining: Math.max(0, maxChars) };
|
|
89
|
+
return {
|
|
90
|
+
progressSummary: limited(content.progressSummary, budget),
|
|
91
|
+
lastError: content.lastError === null ? null : limited(content.lastError, budget),
|
|
92
|
+
completed: limitedList(content.completed, budget),
|
|
93
|
+
pendingTasks: limitedList(content.pendingTasks, budget),
|
|
94
|
+
decisions: limitedList(content.decisions, budget),
|
|
95
|
+
risks: limitedList(content.risks, budget),
|
|
96
|
+
relatedFiles: limitedList(content.relatedFiles, budget),
|
|
97
|
+
branchOrCommitDrift,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function isContextMetadata(value) {
|
|
101
|
+
return isRecord(value)
|
|
102
|
+
&& value.schemaVersion === 1
|
|
103
|
+
&& typeof value.id === 'string'
|
|
104
|
+
&& typeof value.title === 'string'
|
|
105
|
+
&& typeof value.projectRoot === 'string'
|
|
106
|
+
&& (typeof value.currentSnapshotId === 'string' || value.currentSnapshotId === null)
|
|
107
|
+
&& ['draft', 'active', 'stable', 'archived', 'deleted'].includes(value.status);
|
|
108
|
+
}
|
|
109
|
+
function matchedContext(context, content, queryTerms) {
|
|
110
|
+
const values = {
|
|
111
|
+
title: context.title,
|
|
112
|
+
progressSummary: content.progressSummary,
|
|
113
|
+
lastError: content.lastError ?? '',
|
|
114
|
+
completed: content.completed.join(' '),
|
|
115
|
+
pendingTasks: content.pendingTasks.join(' '),
|
|
116
|
+
decisions: content.decisions.join(' '),
|
|
117
|
+
risks: content.risks.join(' '),
|
|
118
|
+
relatedFiles: content.relatedFiles.join(' '),
|
|
119
|
+
};
|
|
120
|
+
const matchedFields = new Set();
|
|
121
|
+
const matchedTerms = new Set();
|
|
122
|
+
let score = 0;
|
|
123
|
+
for (const term of queryTerms) {
|
|
124
|
+
for (const [field, value] of Object.entries(values)) {
|
|
125
|
+
if (normalized(value).includes(term)) {
|
|
126
|
+
matchedFields.add(field);
|
|
127
|
+
matchedTerms.add(term);
|
|
128
|
+
score += field === 'title' ? 5 : 3;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { matchedFields: [...matchedFields], matchedTerms: [...matchedTerms], score };
|
|
133
|
+
}
|
|
134
|
+
export async function saveContext(startPath, input, options = {}) {
|
|
135
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
136
|
+
return { projectRoot, result: await checkpoint(projectRoot, input, options) };
|
|
137
|
+
}
|
|
138
|
+
export async function loadContext(startPath, options = {}) {
|
|
139
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
140
|
+
const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
|
|
141
|
+
const knowledge = options.query === undefined ? null : await discoverKnowledge(projectRoot, options.query, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
|
|
142
|
+
const verifiedDeadends = await listDeadends(projectRoot);
|
|
143
|
+
return {
|
|
144
|
+
projectRoot,
|
|
145
|
+
context: result.context,
|
|
146
|
+
snapshot: result.snapshot,
|
|
147
|
+
resumeSummary: result.resumeSummary,
|
|
148
|
+
...(options.includeDetails ? { details: result.details } : {}),
|
|
149
|
+
knowledge,
|
|
150
|
+
verifiedDeadendIds: verifiedDeadends.map((item) => item.deadendId),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export async function contextOrient(startPath, options = {}) {
|
|
154
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
155
|
+
const query = options.query === undefined ? null : options.query.trim();
|
|
156
|
+
if (query === '')
|
|
157
|
+
throw new Error('context orient query must not be empty');
|
|
158
|
+
let loaded;
|
|
159
|
+
try {
|
|
160
|
+
loaded = await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof Error && error.message === 'no active context found') {
|
|
164
|
+
return {
|
|
165
|
+
status: 'no_context',
|
|
166
|
+
projectRoot,
|
|
167
|
+
query,
|
|
168
|
+
context: null,
|
|
169
|
+
snapshot: null,
|
|
170
|
+
resumeSummary: null,
|
|
171
|
+
knowledge: emptyKnowledge(query ?? ''),
|
|
172
|
+
deadends: emptyDeadends(query ?? ''),
|
|
173
|
+
notes: [],
|
|
174
|
+
truncated: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
const notes = await listNotes(projectRoot, {
|
|
180
|
+
contextId: loaded.context.id,
|
|
181
|
+
openOnly: true,
|
|
182
|
+
limit: options.noteLimit ?? 20,
|
|
183
|
+
maxChars: options.maxChars ?? 4000,
|
|
184
|
+
});
|
|
185
|
+
const knowledge = query === null
|
|
186
|
+
? emptyKnowledge('')
|
|
187
|
+
: await discoverKnowledge(projectRoot, query, {
|
|
188
|
+
scope: options.scope,
|
|
189
|
+
limit: options.knowledgeLimit ?? 10,
|
|
190
|
+
maxChars: options.maxChars ?? 4000,
|
|
191
|
+
});
|
|
192
|
+
const deadends = query === null
|
|
193
|
+
? emptyDeadends('')
|
|
194
|
+
: await discoverDeadends(projectRoot, query, {
|
|
195
|
+
scope: options.scope,
|
|
196
|
+
limit: options.deadendLimit ?? 10,
|
|
197
|
+
maxChars: options.maxChars ?? 4000,
|
|
198
|
+
});
|
|
199
|
+
const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
|
|
200
|
+
const resumeSummary = {
|
|
201
|
+
progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
|
|
202
|
+
lastError: loaded.resumeSummary.lastError === null ? null : limited(loaded.resumeSummary.lastError, budget),
|
|
203
|
+
completed: limitedList(loaded.resumeSummary.completed, budget),
|
|
204
|
+
pendingTasks: limitedList(loaded.resumeSummary.pendingTasks, budget),
|
|
205
|
+
decisions: limitedList(loaded.resumeSummary.decisions, budget),
|
|
206
|
+
risks: limitedList(loaded.resumeSummary.risks, budget),
|
|
207
|
+
relatedFiles: limitedList(loaded.resumeSummary.relatedFiles, budget),
|
|
208
|
+
branchOrCommitDrift: loaded.resumeSummary.branchOrCommitDrift,
|
|
209
|
+
};
|
|
210
|
+
const resumeTruncated = textLength(resumeSummary.progressSummary) < textLength(loaded.resumeSummary.progressSummary)
|
|
211
|
+
|| textLength(resumeSummary.lastError) < textLength(loaded.resumeSummary.lastError)
|
|
212
|
+
|| textLength(resumeSummary.completed) < textLength(loaded.resumeSummary.completed)
|
|
213
|
+
|| textLength(resumeSummary.pendingTasks) < textLength(loaded.resumeSummary.pendingTasks)
|
|
214
|
+
|| textLength(resumeSummary.decisions) < textLength(loaded.resumeSummary.decisions)
|
|
215
|
+
|| textLength(resumeSummary.risks) < textLength(loaded.resumeSummary.risks)
|
|
216
|
+
|| textLength(resumeSummary.relatedFiles) < textLength(loaded.resumeSummary.relatedFiles);
|
|
217
|
+
const limitedKnowledgeResult = limitKnowledge(knowledge, budget);
|
|
218
|
+
const limitedDeadendResult = limitDeadends(deadends, budget);
|
|
219
|
+
const limitedNotesResult = limitNotes(notes, budget);
|
|
220
|
+
return {
|
|
221
|
+
status: 'ready',
|
|
222
|
+
projectRoot,
|
|
223
|
+
query,
|
|
224
|
+
context: {
|
|
225
|
+
id: loaded.context.id,
|
|
226
|
+
title: loaded.context.title,
|
|
227
|
+
status: loaded.context.status,
|
|
228
|
+
branch: loaded.context.branch,
|
|
229
|
+
commit: loaded.context.commit,
|
|
230
|
+
updatedAt: loaded.context.updatedAt,
|
|
231
|
+
},
|
|
232
|
+
snapshot: {
|
|
233
|
+
snapshotId: loaded.snapshot.snapshotId,
|
|
234
|
+
mode: loaded.snapshot.mode,
|
|
235
|
+
createdAt: loaded.snapshot.createdAt,
|
|
236
|
+
branch: loaded.snapshot.branch,
|
|
237
|
+
commit: loaded.snapshot.commit,
|
|
238
|
+
},
|
|
239
|
+
resumeSummary,
|
|
240
|
+
knowledge: limitedKnowledgeResult.result,
|
|
241
|
+
deadends: limitedDeadendResult.result,
|
|
242
|
+
notes: limitedNotesResult.notes,
|
|
243
|
+
truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export async function searchContext(startPath, query, options = {}) {
|
|
247
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
248
|
+
const cleanQuery = query.trim();
|
|
249
|
+
if (!cleanQuery)
|
|
250
|
+
throw new Error('context search query must not be empty');
|
|
251
|
+
const queryTerms = terms(cleanQuery);
|
|
252
|
+
const contextIds = options.contextId ? [options.contextId] : await readdir(contextsRoot(projectRoot));
|
|
253
|
+
const matches = [];
|
|
254
|
+
for (const contextId of contextIds) {
|
|
255
|
+
const context = await readJson(contextMetadataPath(projectRoot, contextId), isContextMetadata);
|
|
256
|
+
if (!['active', 'stable'].includes(context.status) || !context.currentSnapshotId)
|
|
257
|
+
continue;
|
|
258
|
+
const content = await rebuildSnapshot(projectRoot, context.id, context.currentSnapshotId);
|
|
259
|
+
const match = matchedContext(context, content, queryTerms);
|
|
260
|
+
if (match.matchedTerms.length === 0)
|
|
261
|
+
continue;
|
|
262
|
+
matches.push({
|
|
263
|
+
context,
|
|
264
|
+
snapshotId: context.currentSnapshotId,
|
|
265
|
+
score: match.score,
|
|
266
|
+
matchedTerms: match.matchedTerms,
|
|
267
|
+
matchedFields: match.matchedFields,
|
|
268
|
+
summary: summary(content, options.maxChars ?? 4000),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
matches.sort((left, right) => right.score - left.score || right.context.updatedAt.localeCompare(left.context.updatedAt) || left.context.id.localeCompare(right.context.id));
|
|
272
|
+
const limit = options.limit ?? 10;
|
|
273
|
+
const selected = matches.slice(0, limit);
|
|
274
|
+
const knowledge = await discoverKnowledge(projectRoot, cleanQuery, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
|
|
275
|
+
const deadends = await discoverDeadends(projectRoot, cleanQuery, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
|
|
276
|
+
return {
|
|
277
|
+
projectRoot,
|
|
278
|
+
query: cleanQuery,
|
|
279
|
+
total: matches.length,
|
|
280
|
+
truncated: matches.length > selected.length,
|
|
281
|
+
hits: selected,
|
|
282
|
+
knowledge,
|
|
283
|
+
verifiedDeadendIds: deadends.hits.map((hit) => hit.deadend.deadendId),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export * from './api.js';
|
|
2
|
+
export type { ContextOrientContext, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
|
|
3
|
+
export type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, ContextSearchHit, ContextSearchOptions, ContextSearchResult, } from '../runtime/types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api.js';
|