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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -136
  3. package/dist/src/agent/api.d.ts +8 -0
  4. package/dist/src/agent/api.js +285 -0
  5. package/dist/src/agent/index.d.ts +3 -0
  6. package/dist/src/agent/index.js +1 -0
  7. package/dist/src/agent/types.d.ts +50 -0
  8. package/dist/src/agent/types.js +1 -0
  9. package/dist/src/capture/context-filter.d.ts +7 -0
  10. package/dist/src/cli/main.d.ts +2 -0
  11. package/dist/src/cli/main.js +382 -30
  12. package/dist/src/git/git-adapter.d.ts +2 -0
  13. package/dist/src/hooks/claude-code.d.ts +1 -0
  14. package/dist/src/hooks/claude-code.js +84 -0
  15. package/dist/src/integrations/claude-code.d.ts +19 -0
  16. package/dist/src/integrations/claude-code.js +122 -0
  17. package/dist/src/mcp/main.d.ts +1 -0
  18. package/dist/src/mcp/main.js +5 -0
  19. package/dist/src/mcp/server.d.ts +2 -0
  20. package/dist/src/mcp/server.js +40 -0
  21. package/dist/src/project/project-resolver.d.ts +1 -0
  22. package/dist/src/runtime/diagnostics.d.ts +6 -0
  23. package/dist/src/runtime/diagnostics.js +34 -5
  24. package/dist/src/runtime/init.d.ts +7 -0
  25. package/dist/src/runtime/knowledge.d.ts +9 -0
  26. package/dist/src/runtime/knowledge.js +179 -12
  27. package/dist/src/runtime/notes.d.ts +6 -0
  28. package/dist/src/runtime/notes.js +173 -0
  29. package/dist/src/runtime/runtime.d.ts +11 -0
  30. package/dist/src/runtime/runtime.js +48 -18
  31. package/dist/src/runtime/types.d.ts +294 -0
  32. package/dist/src/storage/atomic-write.d.ts +1 -0
  33. package/dist/src/storage/json-store.d.ts +6 -0
  34. package/dist/src/storage/layout.d.ts +13 -0
  35. package/dist/src/storage/layout.js +3 -0
  36. package/dist/src/storage/lock.d.ts +1 -0
  37. package/dist/src/version.d.ts +1 -0
  38. package/dist/src/version.js +1 -0
  39. package/package.json +48 -11
  40. 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 是一个面向 AI 编程会话的本地上下文运行时 CLI。它把一次任务的进度、决策、待办、风险、知识和已验证死路保存为可恢复的结构化状态,帮助 Agent 在新会话中快速完成交接,而不是重新阅读大量项目内容。
3
+ `fluffy-context` 是面向 AI 编程会话的本地 Context Runtime CLI。0.3.0 提供可重复、安全调用的任务定位入口、MCP 工具,以及 Claude Code 的阶段性工作流引导,让 Agent 不必自行拼接恢复、共识发现与短期记录。
4
4
 
5
- 它更接近“Context 的 Git”,而不是代码备份工具:Git 仍然负责代码和真实文件变更,Context Runtime 负责 AI 工作状态的版本化保存与恢复。
5
+ 它更接近“Context 的 Git”,而不是代码备份工具:Git 仍然负责代码和真实文件变更;Context Runtime 负责 AI 工作状态、已验证共识与交接信息的本地保存和恢复。
6
6
 
7
7
  ## 适用场景
8
8
 
9
- - 下班前保存当前任务状态,第二天继续工作。
10
- - 保存完成项、待办、阻塞、决策和风险。
11
- - 将可复用业务知识独立记录,并在确认后纳入知识库。
12
- - 记录已经验证不可行的方案,避免新会话重复探索。
13
- - 通过摘要预算恢复上下文,减少不必要的 Token 消耗。
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 可选。项目位于 Git 仓库内时,CLI 会记录当前分支和 commit 信息。
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
- 在项目根目录初始化 Context Runtime:
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/state-machine.test.ts"
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 resume
57
+ ctx orient "订单状态机异常路径" --max-chars 4000
72
58
  ```
73
59
 
74
- `resume` 默认返回轻量摘要,同时保留完整详情供按需使用。可以限制摘要字符数:
60
+ `ctx orient` 只读地聚合当前 Context 摘要、与查询匹配的**已验证** Knowledge 和 Deadend,以及当前 Context 尚未吸收的 Note。它不更新 `lastUsedAt`、不创建 Snapshot,也不会记录 usage 噪声。省略查询时可只恢复当前任务和 open Notes:
75
61
 
76
62
  ```bash
77
- ctx resume --max-chars 2000
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
- 初始化项目的 Context Runtime 存储布局。重复执行是幂等的,不会覆盖已有 Context 或项目忽略规则。
69
+ 推荐阶段:
85
70
 
86
- ```bash
87
- ctx init
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
- ### `ctx checkpoint`
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
- 保存结构化工作状态。第一次保存创建 baseline,后续有变化时保存 patch;内容没有变化时返回 `no_change`。
83
+ 业务命令默认输出格式化 JSON;帮助输出为纯文本。错误写入标准错误并返回非零退出码。`ctx --version` 输出一个 JSON 字符串。
95
84
 
96
85
  ```bash
97
- ctx checkpoint \
98
- --context <context-id> \
99
- --title "修复支付回调" \
100
- --progress "已定位签名校验失败原因" \
101
- --last-error "测试环境缺少回调凭据" \
102
- --completed "复现问题,确认签名字段" \
103
- --pending "补充回归测试" \
104
- --decisions "保留原始请求体用于验签" \
105
- --risks "旧版客户端字段格式不同" \
106
- --files "src/payment/webhook.ts"
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
- 列表参数使用逗号分隔。`--context` 省略时会创建新的 Context。默认情况下,实际保存之间至少间隔 10 秒;短时间内有变化的保存会返回 `rate_limited`,没有变化则返回 `no_change`。
99
+ ### `ctx checkpoint` 和 `ctx resume`
110
100
 
111
- ### `ctx resume`
101
+ 第一次 checkpoint 创建 baseline,后续变化创建 patch。相同内容返回 `no_change`;短时间内的变化可能返回 `rate_limited`,应在完成更多阶段工作后再保存。`ctx resume` 返回轻量摘要和按需使用的完整 details,并会保留其既有的 `lastUsedAt` 更新语义。
112
102
 
113
- 恢复一个 active 或 stable Context。默认优先选择当前分支上最近更新的 Context,并返回 Git 分支/commit 是否发生漂移。
103
+ ### `ctx note` 和 `ctx activity`
114
104
 
115
- ```bash
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
- ### `ctx status`
107
+ ### Knowledge 与 Deadend
122
108
 
123
- 查看项目是否已初始化以及当前 Context 索引:
109
+ `ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all` 审查。
124
110
 
125
111
  ```bash
126
- ctx status
112
+ ctx learn "订单取消后不能再次进入支付中状态" --scope project
113
+ ctx deadend --attempt "使用共享可变单例保存订单状态" --reason "并发测试出现跨用例状态泄漏"
114
+ ctx knowledge discover "订单取消"
127
115
  ```
128
116
 
129
- ### `ctx doctor`
117
+ ## MCP 集成
130
118
 
131
- 检查 Manifest、存储目录、索引、Context 元数据、当前 Snapshot 和索引重建一致性:
119
+ `ctx agent serve` 启动一个 MCP stdio server,暴露一个工具:`context_orient`。该 server 的标准输入和输出均属于 MCP 协议,不能在交互式终端直接使用或混入日志。
132
120
 
133
121
  ```bash
134
- ctx doctor
122
+ ctx agent serve
135
123
  ```
136
124
 
137
- ### `ctx learn`
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
- 记录一条待确认的项目知识。Knowledge 默认是 `candidate`,不会进入普通的已验证知识列表。
129
+ 先查看项目是否需要安装集成:
140
130
 
141
131
  ```bash
142
- ctx learn "订单取消后不能再次进入支付中状态" \
143
- --scope project \
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 knowledge
153
- ctx knowledge --all
154
- ctx knowledge verify <knowledge-id>
139
+ ctx integrate claude install --apply
155
140
  ```
156
141
 
157
- 不提供 `--context` 时,知识仍可以记录为项目级候选知识;如果提供来源,则对应 Context 和 Snapshot 必须存在。
142
+ 安装会以幂等方式合并项目本地配置:
158
143
 
159
- ### `ctx deadend`
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
- 记录一条已尝试但不可行的路径。Deadend 默认是 `candidate`,不会默认注入恢复摘要。
147
+ 安装会保留无关的 Hook、权限和 MCP server;遇到无效 JSON、无效相关结构或同名冲突配置会拒绝覆盖。Hook 仅注入有界的 Orient/Plan/Implement/Verify/Handoff 指引,输入损坏、项目未初始化或查询失败时会 fail open,不阻塞 Claude Code 任务,也不会自动创建 checkpoint。
162
148
 
163
- ```bash
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
- ```bash
175
- ctx deadends
176
- ctx deadends --all
177
- ctx deadend verify <deadend-id>
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
- ## `.contextignored`
162
+ `fluffy-context/agent` 提供相同的 Agent API。公开类型声明随包发布。Runtime 的存储布局与内部模块不是兼容性承诺。
163
+
164
+ ## `.contextignored` 与安全边界
181
165
 
182
- `ctx init` 会在项目根目录创建 `.contextignored`。它用于配置不应作为上下文关联文件保存的路径,语法接近 `.gitignore`:
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
- 内置保护规则始终优先,包括 `.context/`、`.git/`、依赖目录、构建产物、环境变量文件、密钥和常见凭据文件。项目规则不能通过否定规则覆盖这些保护。
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
- CLI 默认向标准输出写入格式化 JSON,适合被脚本、Agent 或 IDE 集成:
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
- 本项目核心运行时只依赖 Node.js 内置模块,当前没有运行时第三方依赖。
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
- 并提供基础的 Knowledge、Deadend、忽略规则、诊断、Snapshot 增量保存、摘要预算和保存限流能力。远程同步、多人协作、复杂语义检索、自动模型总结和 Context merge 不属于当前版本的已实现能力。
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';