fluffy-context 0.7.6 → 0.7.7
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/README.md +27 -9
- package/dist/src/agent/api.d.ts +8 -4
- package/dist/src/agent/api.js +146 -131
- package/dist/src/agent/index.d.ts +1 -0
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli/main.js +59 -11
- package/dist/src/cognition/journal.js +13 -1
- package/dist/src/cognition/types.d.ts +16 -2
- package/dist/src/cognition/usage-report.d.ts +40 -0
- package/dist/src/cognition/usage-report.js +232 -0
- package/dist/src/cognition/usage.d.ts +5 -0
- package/dist/src/cognition/usage.js +48 -0
- package/dist/src/compiler/compile.js +1 -1
- package/dist/src/compiler/identity.js +1 -1
- package/dist/src/compiler/sources.js +1 -1
- package/dist/src/compiler/types.d.ts +1 -1
- package/dist/src/mcp/server.js +27 -6
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/fluffy-context/SKILL.md +23 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本地优先、Agent 无关的 Context 与 Knowledge Runtime,帮助 AI 编程 Agent 保存任务状态、恢复工作上下文、复用已验证共识,并生成有界且可追溯的上下文输入。
|
|
4
4
|
|
|
5
|
-
**当前版本:0.7.
|
|
5
|
+
**当前版本:0.7.7**
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
checkpoint → orient → compile → expand → feedback → Agent continues
|
|
@@ -21,7 +21,7 @@ checkpoint → orient → compile → expand → feedback → Agent continues
|
|
|
21
21
|
- `ctx compile`:生成带预算、来源和排除原因的 provider-neutral Context Manifest。
|
|
22
22
|
- `ctx learn` / `ctx deadend`:记录可复用结论和失败方案,之后显式验证再用于普通检索。
|
|
23
23
|
|
|
24
|
-
## 0.7.
|
|
24
|
+
## 0.7.7 当前能力与边界
|
|
25
25
|
|
|
26
26
|
| 能力 | 当前支持 |
|
|
27
27
|
| --- | --- |
|
|
@@ -32,16 +32,17 @@ checkpoint → orient → compile → expand → feedback → Agent continues
|
|
|
32
32
|
| 文件活动 | 可选记录经过过滤的文件 metadata,并编译为 `activity` section |
|
|
33
33
|
| Runtime | 本地 v2 Runtime、Git 观察、可选文件观察、自动唤醒和 fail-open 集成 |
|
|
34
34
|
| 治理 | Knowledge/Deadend 的 candidate、verify、deprecate/obsolete、reject |
|
|
35
|
-
| 集成 | Claude Code Hook、Claude Code 配置集成、MCP `context_orient`/`context_expand`、TypeScript Agent API |
|
|
35
|
+
| 集成 | Claude Code Hook、Claude Code 配置集成、MCP `context_orient`/`context_expand`/`context_usage_report`、TypeScript Agent API |
|
|
36
|
+
| 观测 | `ctx usage report`:只读统计资产、显式复用、操作趋势和 estimated token 投入 |
|
|
36
37
|
|
|
37
|
-
以下能力**不属于 0.7.
|
|
38
|
+
以下能力**不属于 0.7.7**:
|
|
38
39
|
|
|
39
|
-
- 没有 `ctx clean
|
|
40
|
+
- 没有 `ctx clean`;`ctx usage report` 是当前支持的只读观测命令。
|
|
40
41
|
- 不自动创建 checkpoint、Note、Knowledge 或 Deadend。
|
|
41
42
|
- 不自动把 Candidate 提升为 Verified。
|
|
42
43
|
- 不提供模型总结、Embedding/向量检索、远程同步、多人协作或 Context merge。
|
|
43
44
|
|
|
44
|
-
0.7.
|
|
45
|
+
0.7.7 已支持 `context_expand`,用于按需取得已选候选的 `structured` 或 `evidence` 层;它是无状态、只读操作,不读取项目文件内容,也不自动记录 feedback。
|
|
45
46
|
|
|
46
47
|
## 快速开始
|
|
47
48
|
|
|
@@ -180,7 +181,7 @@ Manifest 主要包含:
|
|
|
180
181
|
- `identity`:Manifest hash、选中 item hash 和 estimated token;
|
|
181
182
|
- `text`:最终有界文本。
|
|
182
183
|
|
|
183
|
-
0.7.
|
|
184
|
+
0.7.7 的候选项提供:
|
|
184
185
|
|
|
185
186
|
```text
|
|
186
187
|
level: signal | summary | structured | evidence
|
|
@@ -212,6 +213,23 @@ Compiler 具有以下边界:
|
|
|
212
213
|
- 普通查询默认只纳入适用且已验证的 Knowledge 和 Deadend;
|
|
213
214
|
- `context_expand` 只读取已选候选的安全 metadata,不读取任意项目文件内容。
|
|
214
215
|
|
|
216
|
+
### `ctx usage report`
|
|
217
|
+
|
|
218
|
+
`usage report` 适合每周或每月复盘,不需要为每条 prompt 调用。它是只读的,基于 v1 文件和 v2 journal 派生统计,不创建事件、不修改 Context、Snapshot、Note 或 projection:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
ctx usage report --days 30
|
|
222
|
+
ctx usage report --month 2026-08 --granularity week
|
|
223
|
+
ctx usage report --since 2026-08-01T00:00:00.000Z \
|
|
224
|
+
--until 2026-09-01T00:00:00.000Z \
|
|
225
|
+
--baseline-tokens 12000 --baseline-source manual
|
|
226
|
+
ctx usage report --format ascii
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
默认输出稳定 JSON;`--format ascii` 仅输出终端仪表盘。报告展示资产总量和窗口增量、verified Knowledge/Deadend、显式 `record.used` reuse、orient/compile/expand/resume 操作统计、趋势以及上下文输出投入。
|
|
230
|
+
|
|
231
|
+
报告中的 `chars / 4` 是 `estimated` token,不是真实 Provider token;Provider token 和计费金额当前为 `unavailable`。没有显式 `--baseline-tokens` 时,不会虚构 estimated savings、ROI 或 payback;查询命中、候选选择和编译纳入也不等于 reuse,只有显式 `record.used` 事件才计入复用。报告不替代 `ctx doctor`、`ctx journal verify` 或 Knowledge/Deadend 的人工治理。
|
|
232
|
+
|
|
215
233
|
## Knowledge、Deadend 和 Note
|
|
216
234
|
|
|
217
235
|
### Knowledge:记录可复用认知
|
|
@@ -423,7 +441,7 @@ ctx integrate claude install --apply
|
|
|
423
441
|
ctx agent serve
|
|
424
442
|
```
|
|
425
443
|
|
|
426
|
-
0.7.
|
|
444
|
+
0.7.7 当前暴露以下工具:
|
|
427
445
|
|
|
428
446
|
```text
|
|
429
447
|
context_orient 只读、有界地获取任务上下文
|
|
@@ -609,7 +627,7 @@ npm pack --dry-run --json
|
|
|
609
627
|
|
|
610
628
|
## 未来方向
|
|
611
629
|
|
|
612
|
-
以下方向属于后续版本,不是 0.7.
|
|
630
|
+
以下方向属于后续版本,不是 0.7.7 的当前能力:
|
|
613
631
|
|
|
614
632
|
| 方向 | 目标 |
|
|
615
633
|
| --- | --- |
|
package/dist/src/agent/api.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { type UsageInterface } from '../cognition/usage.js';
|
|
2
|
+
import { type UsageReport, type UsageReportOptions } from '../cognition/usage-report.js';
|
|
1
3
|
import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
|
|
2
4
|
import type { ContextOrientOptions, ContextOrientResult, ContextUseInput } from './types.js';
|
|
3
|
-
import type { ContextExpandInput, ContextExpandResult } from '../compiler/types.js';
|
|
5
|
+
import type { ContextExpandInput, ContextExpandResult, ContextCompileOptions, ContextManifest } from '../compiler/types.js';
|
|
4
6
|
export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
|
|
5
7
|
minSaveIntervalMs?: number;
|
|
6
8
|
}): Promise<AgentSaveResult>;
|
|
7
|
-
export declare function contextExpand(startPath: string | undefined, input: ContextExpandInput): Promise<ContextExpandResult>;
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function contextExpand(startPath: string | undefined, input: ContextExpandInput, interfaceName?: UsageInterface): Promise<ContextExpandResult>;
|
|
10
|
+
export declare function contextCompile(startPath: string | undefined, options?: ContextCompileOptions, interfaceName?: UsageInterface): Promise<ContextManifest>;
|
|
11
|
+
export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions, interfaceName?: UsageInterface): Promise<AgentLoadResult>;
|
|
12
|
+
export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions, interfaceName?: UsageInterface | null): Promise<ContextOrientResult>;
|
|
13
|
+
export declare function contextUsageReport(startPath: string | undefined, options?: UsageReportOptions): Promise<UsageReport>;
|
|
10
14
|
export declare function recordContextUse(startPath: string | undefined, input: ContextUseInput): Promise<{
|
|
11
15
|
recorded: boolean;
|
|
12
16
|
eventId: string;
|
package/dist/src/agent/api.js
CHANGED
|
@@ -9,6 +9,9 @@ import { discoverV2Deadends, discoverV2Knowledge } from '../cognition/retrieval.
|
|
|
9
9
|
import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
|
|
10
10
|
import { listNotes } from '../runtime/notes.js';
|
|
11
11
|
import { expandContext } from '../compiler/expand.js';
|
|
12
|
+
import { compileContext } from '../compiler/compile.js';
|
|
13
|
+
import { withUsageTelemetry } from '../cognition/usage.js';
|
|
14
|
+
import { buildUsageReport } from '../cognition/usage-report.js';
|
|
12
15
|
function normalized(value) {
|
|
13
16
|
return value.trim().toLocaleLowerCase();
|
|
14
17
|
}
|
|
@@ -139,151 +142,163 @@ export async function saveContext(startPath, input, options = {}) {
|
|
|
139
142
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
140
143
|
return { projectRoot, result: await checkpoint(projectRoot, input, options) };
|
|
141
144
|
}
|
|
142
|
-
export async function contextExpand(startPath, input) {
|
|
143
|
-
|
|
144
|
-
return expandContext(projectRoot, input);
|
|
145
|
+
export async function contextExpand(startPath, input, interfaceName = 'agent') {
|
|
146
|
+
return withUsageTelemetry(startPath, 'expand', 'agent', input, () => resolveProjectRoot(startPath).then((projectRoot) => expandContext(projectRoot, input)), interfaceName);
|
|
145
147
|
}
|
|
146
|
-
export async function
|
|
147
|
-
|
|
148
|
-
const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
|
|
149
|
-
const knowledge = options.query === undefined ? null : await discoverKnowledge(projectRoot, options.query, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
|
|
150
|
-
const verifiedDeadends = await listDeadends(projectRoot);
|
|
151
|
-
return {
|
|
152
|
-
projectRoot,
|
|
153
|
-
context: result.context,
|
|
154
|
-
snapshot: result.snapshot,
|
|
155
|
-
resumeSummary: result.resumeSummary,
|
|
156
|
-
...(options.includeDetails ? { details: result.details } : {}),
|
|
157
|
-
knowledge,
|
|
158
|
-
verifiedDeadendIds: verifiedDeadends.map((item) => item.deadendId),
|
|
159
|
-
};
|
|
148
|
+
export async function contextCompile(startPath, options = {}, interfaceName = 'agent') {
|
|
149
|
+
return withUsageTelemetry(startPath, 'compile', 'agent', options, () => compileContext(startPath, options), interfaceName);
|
|
160
150
|
}
|
|
161
|
-
export async function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
truncation: { summary: false, knowledge: false, deadends: false, notes: false },
|
|
191
|
-
},
|
|
192
|
-
};
|
|
151
|
+
export async function loadContext(startPath, options = {}, interfaceName = 'agent') {
|
|
152
|
+
return withUsageTelemetry(startPath, 'resume', 'agent', options, async () => {
|
|
153
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
154
|
+
const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
|
|
155
|
+
const knowledge = options.query === undefined ? null : await discoverKnowledge(projectRoot, options.query, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
|
|
156
|
+
const verifiedDeadends = await listDeadends(projectRoot);
|
|
157
|
+
return {
|
|
158
|
+
projectRoot,
|
|
159
|
+
context: result.context,
|
|
160
|
+
snapshot: result.snapshot,
|
|
161
|
+
resumeSummary: result.resumeSummary,
|
|
162
|
+
...(options.includeDetails ? { details: result.details } : {}),
|
|
163
|
+
knowledge,
|
|
164
|
+
verifiedDeadendIds: verifiedDeadends.map((item) => item.deadendId),
|
|
165
|
+
};
|
|
166
|
+
}, interfaceName);
|
|
167
|
+
}
|
|
168
|
+
export async function contextOrient(startPath, options = {}, interfaceName = 'agent') {
|
|
169
|
+
const action = async () => {
|
|
170
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
171
|
+
const query = options.query === undefined ? null : options.query.trim();
|
|
172
|
+
if (query === '')
|
|
173
|
+
throw new Error('context orient query must not be empty');
|
|
174
|
+
let loaded;
|
|
175
|
+
try {
|
|
176
|
+
const v2 = await selectV2Orientation(projectRoot, options.contextId);
|
|
177
|
+
loaded = v2
|
|
178
|
+
? await resumeSnapshot(projectRoot, v2.contextId, v2.snapshotId, options.maxChars ?? 4000)
|
|
179
|
+
: await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
|
|
193
180
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof Error && error.message === 'no active context found') {
|
|
183
|
+
return {
|
|
184
|
+
status: 'no_context',
|
|
185
|
+
projectRoot,
|
|
186
|
+
query,
|
|
187
|
+
context: null,
|
|
188
|
+
snapshot: null,
|
|
189
|
+
resumeSummary: null,
|
|
190
|
+
knowledge: emptyKnowledge(query ?? ''),
|
|
191
|
+
deadends: emptyDeadends(query ?? ''),
|
|
192
|
+
notes: [],
|
|
193
|
+
truncated: false,
|
|
194
|
+
explanation: {
|
|
195
|
+
mode: 'v1-fallback',
|
|
196
|
+
selection: 'v1',
|
|
197
|
+
retrieval: { knowledge: {}, deadends: {} },
|
|
198
|
+
truncation: { summary: false, knowledge: false, deadends: false, notes: false },
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
const v2 = await selectV2Orientation(projectRoot, options.contextId);
|
|
205
|
+
const notes = (await listNotes(projectRoot, {
|
|
206
|
+
contextId: loaded.context.id,
|
|
207
|
+
openOnly: true,
|
|
208
|
+
limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
|
|
209
|
+
maxChars: options.maxChars ?? 4000,
|
|
210
|
+
})).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
|
|
211
|
+
const v2Knowledge = query === null || !v2 ? null : discoverV2Knowledge(v2.retrieval, query, v2.currentGit, {
|
|
216
212
|
scope: options.scope,
|
|
217
213
|
limit: options.knowledgeLimit ?? 10,
|
|
218
214
|
maxChars: options.maxChars ?? 4000,
|
|
219
215
|
});
|
|
220
|
-
|
|
221
|
-
? emptyDeadends('')
|
|
222
|
-
: v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
|
|
216
|
+
const v2Deadends = query === null || !v2 ? null : discoverV2Deadends(v2.retrieval, query, v2.currentGit, {
|
|
223
217
|
scope: options.scope,
|
|
224
218
|
limit: options.deadendLimit ?? 10,
|
|
225
219
|
maxChars: options.maxChars ?? 4000,
|
|
226
220
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
221
|
+
const applicableKnowledge = query === null
|
|
222
|
+
? emptyKnowledge('')
|
|
223
|
+
: v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
|
|
224
|
+
scope: options.scope,
|
|
225
|
+
limit: options.knowledgeLimit ?? 10,
|
|
226
|
+
maxChars: options.maxChars ?? 4000,
|
|
227
|
+
});
|
|
228
|
+
const applicableDeadends = query === null
|
|
229
|
+
? emptyDeadends('')
|
|
230
|
+
: v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
|
|
231
|
+
scope: options.scope,
|
|
232
|
+
limit: options.deadendLimit ?? 10,
|
|
233
|
+
maxChars: options.maxChars ?? 4000,
|
|
234
|
+
});
|
|
235
|
+
const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
|
|
236
|
+
const resumeSummary = {
|
|
237
|
+
progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
|
|
238
|
+
lastError: loaded.resumeSummary.lastError === null ? null : limited(loaded.resumeSummary.lastError, budget),
|
|
239
|
+
completed: limitedList(loaded.resumeSummary.completed, budget),
|
|
240
|
+
pendingTasks: limitedList(loaded.resumeSummary.pendingTasks, budget),
|
|
241
|
+
decisions: limitedList(loaded.resumeSummary.decisions, budget),
|
|
242
|
+
risks: limitedList(loaded.resumeSummary.risks, budget),
|
|
243
|
+
relatedFiles: limitedList(loaded.resumeSummary.relatedFiles, budget),
|
|
244
|
+
branchOrCommitDrift: loaded.resumeSummary.branchOrCommitDrift,
|
|
245
|
+
};
|
|
246
|
+
const resumeTruncated = textLength(resumeSummary.progressSummary) < textLength(loaded.resumeSummary.progressSummary)
|
|
247
|
+
|| textLength(resumeSummary.lastError) < textLength(loaded.resumeSummary.lastError)
|
|
248
|
+
|| textLength(resumeSummary.completed) < textLength(loaded.resumeSummary.completed)
|
|
249
|
+
|| textLength(resumeSummary.pendingTasks) < textLength(loaded.resumeSummary.pendingTasks)
|
|
250
|
+
|| textLength(resumeSummary.decisions) < textLength(loaded.resumeSummary.decisions)
|
|
251
|
+
|| textLength(resumeSummary.risks) < textLength(loaded.resumeSummary.risks)
|
|
252
|
+
|| textLength(resumeSummary.relatedFiles) < textLength(loaded.resumeSummary.relatedFiles);
|
|
253
|
+
const limitedKnowledgeResult = limitKnowledge(applicableKnowledge, budget);
|
|
254
|
+
const limitedDeadendResult = limitDeadends(applicableDeadends, budget);
|
|
255
|
+
const limitedNotesResult = limitNotes(notes, budget);
|
|
256
|
+
return {
|
|
257
|
+
status: 'ready',
|
|
258
|
+
projectRoot,
|
|
259
|
+
query,
|
|
260
|
+
context: {
|
|
261
|
+
id: loaded.context.id,
|
|
262
|
+
title: loaded.context.title,
|
|
263
|
+
status: loaded.context.status,
|
|
264
|
+
branch: loaded.context.branch,
|
|
265
|
+
commit: loaded.context.commit,
|
|
266
|
+
updatedAt: loaded.context.updatedAt,
|
|
267
|
+
},
|
|
268
|
+
snapshot: {
|
|
269
|
+
snapshotId: loaded.snapshot.snapshotId,
|
|
270
|
+
mode: loaded.snapshot.mode,
|
|
271
|
+
createdAt: loaded.snapshot.createdAt,
|
|
272
|
+
branch: loaded.snapshot.branch,
|
|
273
|
+
commit: loaded.snapshot.commit,
|
|
278
274
|
},
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
275
|
+
resumeSummary,
|
|
276
|
+
knowledge: limitedKnowledgeResult.result,
|
|
277
|
+
deadends: limitedDeadendResult.result,
|
|
278
|
+
notes: limitedNotesResult.notes,
|
|
279
|
+
truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
|
|
280
|
+
explanation: {
|
|
281
|
+
mode: v2 ? 'v2' : 'v1-fallback',
|
|
282
|
+
selection: v2?.selection ?? 'v1',
|
|
283
|
+
retrieval: {
|
|
284
|
+
knowledge: v2Knowledge?.exclusions ?? {},
|
|
285
|
+
deadends: v2Deadends?.exclusions ?? {},
|
|
286
|
+
},
|
|
287
|
+
truncation: {
|
|
288
|
+
summary: resumeTruncated,
|
|
289
|
+
knowledge: limitedKnowledgeResult.truncated,
|
|
290
|
+
deadends: limitedDeadendResult.truncated,
|
|
291
|
+
notes: limitedNotesResult.truncated,
|
|
292
|
+
},
|
|
284
293
|
},
|
|
285
|
-
}
|
|
294
|
+
};
|
|
286
295
|
};
|
|
296
|
+
return interfaceName === null
|
|
297
|
+
? action()
|
|
298
|
+
: withUsageTelemetry(startPath, 'orient', 'agent', options, action, interfaceName);
|
|
299
|
+
}
|
|
300
|
+
export async function contextUsageReport(startPath, options = {}) {
|
|
301
|
+
return buildUsageReport(startPath, options);
|
|
287
302
|
}
|
|
288
303
|
export async function recordContextUse(startPath, input) {
|
|
289
304
|
return recordUse(startPath, input);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './api.js';
|
|
2
2
|
export type { ContextOrientContext, ContextUseInput, ContextOrientExplanation, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
|
|
3
3
|
export type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, ContextSearchHit, ContextSearchOptions, ContextSearchResult, } from '../runtime/types.js';
|
|
4
|
+
export { contextCompile } from './api.js';
|
|
4
5
|
export { compileContext } from '../compiler/compile.js';
|
|
5
6
|
export { canonicalJson, contentHash, estimateTokens, rebuildManifestHash, validateManifestIdentity } from '../compiler/identity.js';
|
|
6
7
|
export type { ContextActivityRecord, ContextAnchor, ContextBudget, ContextBudgetReport, ContextCandidate, ContextCandidateSection, ContextCompileOptions, ContextExpandInput, ContextExpandLevel, ContextExpandResult, ContextExclusion, ContextExclusionReason, ContextIntent, ContextIntentSignal, ContextLevel, ContextManifest, ContextProvenance, ContextScenario, ContextSourceDescriptor, ContextTruncation, } from '../compiler/types.js';
|
package/dist/src/agent/index.js
CHANGED
package/dist/src/cli/main.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { checkpoint
|
|
3
|
-
import { contextOrient } from '../agent/api.js';
|
|
4
|
-
import {
|
|
5
|
-
import { contextExpand } from '../agent/api.js';
|
|
2
|
+
import { checkpoint } from '../runtime/runtime.js';
|
|
3
|
+
import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext } from '../agent/api.js';
|
|
4
|
+
import { formatUsageReport } from '../cognition/usage-report.js';
|
|
6
5
|
import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
|
|
7
6
|
import { recordUse } from '../cognition/feedback.js';
|
|
8
7
|
import { verifyJournal } from '../cognition/journal.js';
|
|
@@ -82,6 +81,34 @@ Initialize the local .context runtime layout. Re-running init is safe.
|
|
|
82
81
|
|
|
83
82
|
Options:
|
|
84
83
|
--path <path> Project path`,
|
|
84
|
+
usage: `usage: ctx usage report [options]
|
|
85
|
+
|
|
86
|
+
Show read-only context asset, reuse, token investment, and operation metrics.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--path <path> Project path
|
|
90
|
+
--since <ISO> Window start
|
|
91
|
+
--until <ISO> Window end
|
|
92
|
+
--days <number> Rolling window length (default: 30)
|
|
93
|
+
--month <YYYY-MM> Calendar month window
|
|
94
|
+
--granularity <value> day|week|month
|
|
95
|
+
--format <value> json|ascii (default: json)
|
|
96
|
+
--baseline-tokens <number> Explicit comparison token count
|
|
97
|
+
--baseline-source <value> manual|recorded-control`,
|
|
98
|
+
'usage report': `usage: ctx usage report [options]
|
|
99
|
+
|
|
100
|
+
Show read-only context asset, reuse, token investment, and operation metrics.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--path <path> Project path
|
|
104
|
+
--since <ISO> Window start
|
|
105
|
+
--until <ISO> Window end
|
|
106
|
+
--days <number> Rolling window length (default: 30)
|
|
107
|
+
--month <YYYY-MM> Calendar month window
|
|
108
|
+
--granularity <value> day|week|month
|
|
109
|
+
--format <value> json|ascii (default: json)
|
|
110
|
+
--baseline-tokens <number> Explicit comparison token count
|
|
111
|
+
--baseline-source <value> manual|recorded-control`,
|
|
85
112
|
checkpoint: `usage: ctx checkpoint [options]
|
|
86
113
|
|
|
87
114
|
Save structured work state as a baseline or incremental snapshot.
|
|
@@ -161,7 +188,7 @@ Start an MCP stdio server for Agent integrations.
|
|
|
161
188
|
Run "ctx agent serve --help" for details.`,
|
|
162
189
|
'agent serve': `usage: ctx agent serve
|
|
163
190
|
|
|
164
|
-
Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, and context_checkpoint tools.
|
|
191
|
+
Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, context_usage_report, and context_checkpoint tools.
|
|
165
192
|
|
|
166
193
|
The server owns standard input/output; do not use it interactively.`,
|
|
167
194
|
hook: `usage: ctx hook claude-code session-start|user-prompt
|
|
@@ -363,7 +390,7 @@ Stop the local runtime through authenticated loopback IPC.`,
|
|
|
363
390
|
Internal runtime daemon entry point.`,
|
|
364
391
|
};
|
|
365
392
|
function usage() {
|
|
366
|
-
return `usage: ctx init|checkpoint|resume|orient|compile|expand|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
|
|
393
|
+
return `usage: ctx init|checkpoint|resume|orient|compile|expand|usage|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
|
|
367
394
|
|
|
368
395
|
Run \"ctx <command> --help\" for command details.`;
|
|
369
396
|
}
|
|
@@ -392,7 +419,7 @@ function printHelp(args) {
|
|
|
392
419
|
|| (command === 'note' && ['add', 'list'].includes(args[1]))
|
|
393
420
|
|| (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
|
|
394
421
|
|| (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
|
|
395
|
-
|| (command === '
|
|
422
|
+
|| (command === 'usage' && args[1] === 'report');
|
|
396
423
|
const key = nested ? `${command} ${args[1]}` : command;
|
|
397
424
|
const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
|
|
398
425
|
const help = HELP[nestedKey];
|
|
@@ -450,7 +477,7 @@ async function run(args) {
|
|
|
450
477
|
case 'resume':
|
|
451
478
|
validateOptions(args.slice(1), ['--path', '--context', '--max-chars'], ['--path', '--context', '--max-chars']);
|
|
452
479
|
validatePositionals(positionals(args.slice(1), ['--path', '--context', '--max-chars']), 0, HELP.resume);
|
|
453
|
-
print(await
|
|
480
|
+
print(await loadContext(target, { contextId: option(args, '--context'), maxChars: numericOption(args, '--max-chars', 4000) }, 'cli'));
|
|
454
481
|
return;
|
|
455
482
|
case 'orient': {
|
|
456
483
|
const valueOptions = ['--path', '--context', '--scope', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit'];
|
|
@@ -466,7 +493,7 @@ async function run(args) {
|
|
|
466
493
|
knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
|
|
467
494
|
deadendLimit: numericOption(args, '--deadend-limit', 10),
|
|
468
495
|
noteLimit: numericOption(args, '--note-limit', 20),
|
|
469
|
-
}));
|
|
496
|
+
}, 'cli'));
|
|
470
497
|
return;
|
|
471
498
|
}
|
|
472
499
|
case 'compile': {
|
|
@@ -474,7 +501,7 @@ async function run(args) {
|
|
|
474
501
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
475
502
|
const query = positionals(args.slice(1), valueOptions);
|
|
476
503
|
validatePositionals(query, 1, HELP.compile);
|
|
477
|
-
print(await
|
|
504
|
+
print(await contextCompile(target, {
|
|
478
505
|
contextId: option(args, '--context'),
|
|
479
506
|
query: query[0],
|
|
480
507
|
scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
|
|
@@ -485,7 +512,7 @@ async function run(args) {
|
|
|
485
512
|
deadendLimit: numericOption(args, '--deadend-limit', 10),
|
|
486
513
|
noteLimit: numericOption(args, '--note-limit', 20),
|
|
487
514
|
activityLimit: numericOption(args, '--activity-limit', 20),
|
|
488
|
-
}));
|
|
515
|
+
}, 'cli'));
|
|
489
516
|
return;
|
|
490
517
|
}
|
|
491
518
|
case 'expand': {
|
|
@@ -521,6 +548,27 @@ async function run(args) {
|
|
|
521
548
|
}));
|
|
522
549
|
return;
|
|
523
550
|
}
|
|
551
|
+
case 'usage': {
|
|
552
|
+
if (args[1] !== 'report')
|
|
553
|
+
throw new Error(HELP.usage);
|
|
554
|
+
const valueOptions = ['--path', '--since', '--until', '--days', '--month', '--granularity', '--format', '--baseline-tokens', '--baseline-source'];
|
|
555
|
+
validateOptions(args.slice(2), valueOptions, valueOptions);
|
|
556
|
+
validatePositionals(positionals(args.slice(2), valueOptions), 0, HELP['usage report']);
|
|
557
|
+
const report = await contextUsageReport(target, {
|
|
558
|
+
since: option(args, '--since'),
|
|
559
|
+
until: option(args, '--until'),
|
|
560
|
+
days: option(args, '--days') === undefined ? undefined : numericOption(args, '--days', 30),
|
|
561
|
+
month: option(args, '--month'),
|
|
562
|
+
granularity: enumOption(args, '--granularity', ['day', 'week', 'month']),
|
|
563
|
+
baselineTokens: option(args, '--baseline-tokens') === undefined ? undefined : numericOption(args, '--baseline-tokens', 0),
|
|
564
|
+
baselineSource: enumOption(args, '--baseline-source', ['manual', 'recorded-control']),
|
|
565
|
+
});
|
|
566
|
+
if (enumOption(args, '--format', ['json', 'ascii']) === 'ascii')
|
|
567
|
+
process.stdout.write(formatUsageReport(report));
|
|
568
|
+
else
|
|
569
|
+
print(report);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
524
572
|
case 'agent':
|
|
525
573
|
if (args[1] !== 'serve')
|
|
526
574
|
throw new Error(HELP.agent);
|
|
@@ -11,9 +11,10 @@ const eventTypes = new Set([
|
|
|
11
11
|
'context.checkpoint.imported', 'context.state.recorded', 'note.recorded', 'note.resolved', 'note.absorbed',
|
|
12
12
|
'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
|
|
13
13
|
'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected', 'record.used',
|
|
14
|
+
'usage.operation.completed',
|
|
14
15
|
'projection.advanced', 'projection.failed',
|
|
15
16
|
]);
|
|
16
|
-
const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'git-observer', 'fs-observer', 'migration']);
|
|
17
|
+
const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'agent', 'git-observer', 'fs-observer', 'migration']);
|
|
17
18
|
const segmentName = '00000001.ndjson';
|
|
18
19
|
function canonical(value) {
|
|
19
20
|
if (value === null || typeof value !== 'object')
|
|
@@ -65,6 +66,17 @@ function validatePayloadBoundary(input) {
|
|
|
65
66
|
if (!isRecord(record) || typeof record.message !== 'string' || record.message.trim().length === 0)
|
|
66
67
|
throw new Error('note message must not be empty');
|
|
67
68
|
}
|
|
69
|
+
if (input.type === 'usage.operation.completed') {
|
|
70
|
+
const requiredNumbers = ['durationMs', 'inputChars', 'outputChars', 'inputBytes', 'outputBytes', 'estimatedInputTokens', 'estimatedOutputTokens'];
|
|
71
|
+
if (payload.operation !== 'orient' && payload.operation !== 'compile' && payload.operation !== 'expand' && payload.operation !== 'resume')
|
|
72
|
+
throw new Error('invalid usage operation');
|
|
73
|
+
if (payload.interface !== 'cli' && payload.interface !== 'mcp' && payload.interface !== 'agent' && payload.interface !== 'hook')
|
|
74
|
+
throw new Error('invalid usage interface');
|
|
75
|
+
if (typeof payload.success !== 'boolean' || payload.estimatedMethod !== 'chars-div-four' || requiredNumbers.some((key) => typeof payload[key] !== 'number' || !Number.isFinite(payload[key]) || payload[key] < 0))
|
|
76
|
+
throw new Error('invalid usage telemetry payload');
|
|
77
|
+
if (!payload.success && typeof payload.errorClass !== 'string')
|
|
78
|
+
throw new Error('usage errors require an error class');
|
|
79
|
+
}
|
|
68
80
|
if (input.type === 'workspace.paths.changed') {
|
|
69
81
|
const changes = payload.changes;
|
|
70
82
|
if (!Array.isArray(changes) || changes.length === 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
|
|
2
|
-
export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'git-observer' | 'fs-observer' | 'migration';
|
|
3
|
-
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'projection.advanced' | 'projection.failed';
|
|
2
|
+
export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'agent' | 'git-observer' | 'fs-observer' | 'migration';
|
|
3
|
+
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'usage.operation.completed' | 'projection.advanced' | 'projection.failed';
|
|
4
4
|
export interface CognitionGitAnchor {
|
|
5
5
|
worktreeId: string | null;
|
|
6
6
|
branch: string | null;
|
|
@@ -67,6 +67,20 @@ export interface CognitionUseFeedback {
|
|
|
67
67
|
entityId: string;
|
|
68
68
|
outcome: 'used';
|
|
69
69
|
}
|
|
70
|
+
export interface UsageOperationCompletedPayload {
|
|
71
|
+
operation: 'orient' | 'compile' | 'expand' | 'resume';
|
|
72
|
+
interface: 'cli' | 'mcp' | 'agent' | 'hook';
|
|
73
|
+
success: boolean;
|
|
74
|
+
durationMs: number;
|
|
75
|
+
inputChars: number;
|
|
76
|
+
outputChars: number;
|
|
77
|
+
inputBytes: number;
|
|
78
|
+
outputBytes: number;
|
|
79
|
+
estimatedInputTokens: number;
|
|
80
|
+
estimatedOutputTokens: number;
|
|
81
|
+
estimatedMethod: 'chars-div-four';
|
|
82
|
+
errorClass?: string;
|
|
83
|
+
}
|
|
70
84
|
export interface CognitionRetrievalEntry {
|
|
71
85
|
entity: 'knowledge' | 'deadend';
|
|
72
86
|
entityId: string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type UsageReportFormat = 'json' | 'ascii';
|
|
2
|
+
export type UsageGranularity = 'day' | 'week' | 'month';
|
|
3
|
+
export interface UsageReportOptions {
|
|
4
|
+
since?: string;
|
|
5
|
+
until?: string;
|
|
6
|
+
days?: number;
|
|
7
|
+
month?: string;
|
|
8
|
+
granularity?: UsageGranularity;
|
|
9
|
+
baselineTokens?: number;
|
|
10
|
+
baselineSource?: 'manual' | 'recorded-control';
|
|
11
|
+
}
|
|
12
|
+
export interface UsageMetric<T> {
|
|
13
|
+
value: T | null;
|
|
14
|
+
quality: 'measured' | 'estimated' | 'derived' | 'unavailable';
|
|
15
|
+
unit: string;
|
|
16
|
+
method?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Window {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
kind: 'rolling' | 'calendar-month' | 'explicit';
|
|
22
|
+
granularity: UsageGranularity;
|
|
23
|
+
}
|
|
24
|
+
export interface UsageReport {
|
|
25
|
+
schemaVersion: 1;
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
window: Window;
|
|
29
|
+
dataQuality: Record<string, unknown>;
|
|
30
|
+
assets: Record<string, unknown>;
|
|
31
|
+
reuse: Record<string, unknown>;
|
|
32
|
+
investment: Record<string, unknown>;
|
|
33
|
+
payback: Record<string, unknown>;
|
|
34
|
+
operations: Record<string, unknown>;
|
|
35
|
+
trends: Array<Record<string, unknown>>;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare function buildUsageReport(startPath: string | undefined, options?: UsageReportOptions): Promise<UsageReport>;
|
|
39
|
+
export declare function formatUsageReport(report: Record<string, any>): string;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
3
|
+
import { readJson, isRecord } from '../storage/json-store.js';
|
|
4
|
+
import { contextsRoot, contextMetadataPath, snapshotsDirectory } from '../storage/layout.js';
|
|
5
|
+
import { readJournal } from './journal.js';
|
|
6
|
+
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
7
|
+
import { listNotes } from '../runtime/notes.js';
|
|
8
|
+
function metric(value, quality, unit, method) {
|
|
9
|
+
return { value, quality, unit, ...(method ? { method } : {}) };
|
|
10
|
+
}
|
|
11
|
+
function iso(value) {
|
|
12
|
+
const time = Date.parse(value);
|
|
13
|
+
if (!Number.isFinite(time))
|
|
14
|
+
throw new Error(`invalid ISO date: ${value}`);
|
|
15
|
+
return new Date(time).toISOString();
|
|
16
|
+
}
|
|
17
|
+
function windowOf(options, now = new Date()) {
|
|
18
|
+
if (options.month && !/^\d{4}-\d{2}$/.test(options.month))
|
|
19
|
+
throw new Error('--month must be YYYY-MM');
|
|
20
|
+
if (options.since && options.month)
|
|
21
|
+
throw new Error('--since and --month cannot be combined');
|
|
22
|
+
if (options.days !== undefined && (!Number.isInteger(options.days) || options.days < 0))
|
|
23
|
+
throw new Error('--days must be a non-negative integer');
|
|
24
|
+
const to = options.until ? iso(options.until) : now.toISOString();
|
|
25
|
+
const toMs = Date.parse(to);
|
|
26
|
+
if (options.month) {
|
|
27
|
+
const [year, month] = options.month.split('-').map(Number);
|
|
28
|
+
const from = new Date(Date.UTC(year, month - 1, 1));
|
|
29
|
+
const end = new Date(Date.UTC(year, month, 1));
|
|
30
|
+
if (month < 1 || month > 12 || end.getUTCMonth() !== month % 12)
|
|
31
|
+
throw new Error('--month must be a valid calendar month');
|
|
32
|
+
return { from: from.toISOString(), to: end.toISOString(), kind: 'calendar-month', granularity: options.granularity ?? 'week' };
|
|
33
|
+
}
|
|
34
|
+
const from = options.since ? iso(options.since) : new Date(toMs - (options.days ?? 30) * 86_400_000).toISOString();
|
|
35
|
+
if (Date.parse(from) > toMs)
|
|
36
|
+
throw new Error('--since must not be after --until');
|
|
37
|
+
return { from, to, kind: options.since || options.until ? 'explicit' : 'rolling', granularity: options.granularity ?? 'day' };
|
|
38
|
+
}
|
|
39
|
+
function inWindow(event, window) {
|
|
40
|
+
const time = Date.parse(event.occurredAt);
|
|
41
|
+
return time >= Date.parse(window.from) && time < Date.parse(window.to);
|
|
42
|
+
}
|
|
43
|
+
function dayStart(value, granularity) {
|
|
44
|
+
const date = new Date(value);
|
|
45
|
+
if (granularity === 'month')
|
|
46
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)).toISOString();
|
|
47
|
+
if (granularity === 'week') {
|
|
48
|
+
const day = date.getUTCDay();
|
|
49
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - (day === 0 ? 6 : day - 1))).toISOString();
|
|
50
|
+
}
|
|
51
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
52
|
+
}
|
|
53
|
+
async function contexts(projectRoot) {
|
|
54
|
+
let ids = [];
|
|
55
|
+
try {
|
|
56
|
+
ids = await readdir(contextsRoot(projectRoot));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { items: [], snapshots: 0 };
|
|
60
|
+
}
|
|
61
|
+
const items = [];
|
|
62
|
+
let snapshots = 0;
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
try {
|
|
65
|
+
const item = await readJson(contextMetadataPath(projectRoot, id), (value) => isRecord(value) && value.schemaVersion === 1 && value.id === id);
|
|
66
|
+
items.push(item);
|
|
67
|
+
snapshots += (await readdir(snapshotsDirectory(projectRoot, id))).filter((file) => file.endsWith('.json')).length;
|
|
68
|
+
}
|
|
69
|
+
catch { /* invalid files are reported by doctor */ }
|
|
70
|
+
}
|
|
71
|
+
return { items, snapshots };
|
|
72
|
+
}
|
|
73
|
+
function usagePayload(event) {
|
|
74
|
+
if (event.type !== 'usage.operation.completed' || !isRecord(event.payload))
|
|
75
|
+
return null;
|
|
76
|
+
const payload = event.payload;
|
|
77
|
+
return (payload.operation === 'orient' || payload.operation === 'compile' || payload.operation === 'expand' || payload.operation === 'resume')
|
|
78
|
+
&& (payload.interface === 'cli' || payload.interface === 'mcp' || payload.interface === 'agent' || payload.interface === 'hook')
|
|
79
|
+
&& typeof payload.success === 'boolean'
|
|
80
|
+
&& typeof payload.durationMs === 'number' && Number.isFinite(payload.durationMs) && payload.durationMs >= 0
|
|
81
|
+
&& typeof payload.inputChars === 'number' && Number.isFinite(payload.inputChars) && payload.inputChars >= 0
|
|
82
|
+
&& typeof payload.outputChars === 'number' && Number.isFinite(payload.outputChars) && payload.outputChars >= 0
|
|
83
|
+
&& typeof payload.inputBytes === 'number' && Number.isFinite(payload.inputBytes) && payload.inputBytes >= 0
|
|
84
|
+
&& typeof payload.outputBytes === 'number' && Number.isFinite(payload.outputBytes) && payload.outputBytes >= 0
|
|
85
|
+
&& typeof payload.estimatedInputTokens === 'number' && Number.isFinite(payload.estimatedInputTokens) && payload.estimatedInputTokens >= 0
|
|
86
|
+
&& typeof payload.estimatedOutputTokens === 'number' && Number.isFinite(payload.estimatedOutputTokens) && payload.estimatedOutputTokens >= 0
|
|
87
|
+
&& payload.estimatedMethod === 'chars-div-four'
|
|
88
|
+
? payload : null;
|
|
89
|
+
}
|
|
90
|
+
function percentile(values, fraction) {
|
|
91
|
+
if (values.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
94
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)];
|
|
95
|
+
}
|
|
96
|
+
function operationStats(events) {
|
|
97
|
+
const groups = new Map();
|
|
98
|
+
for (const event of events) {
|
|
99
|
+
const payload = usagePayload(event);
|
|
100
|
+
if (!payload)
|
|
101
|
+
continue;
|
|
102
|
+
const key = payload.operation;
|
|
103
|
+
groups.set(key, [...(groups.get(key) ?? []), payload]);
|
|
104
|
+
}
|
|
105
|
+
const byOperation = {};
|
|
106
|
+
for (const [operation, values] of groups) {
|
|
107
|
+
const durations = values.map((item) => item.durationMs);
|
|
108
|
+
byOperation[operation] = {
|
|
109
|
+
calls: values.length,
|
|
110
|
+
successes: values.filter((item) => item.success).length,
|
|
111
|
+
failures: values.filter((item) => !item.success).length,
|
|
112
|
+
successRate: values.filter((item) => item.success).length / values.length,
|
|
113
|
+
errorRate: values.filter((item) => !item.success).length / values.length,
|
|
114
|
+
durationMs: { average: durations.reduce((sum, value) => sum + value, 0) / values.length, p50: percentile(durations, 0.5), p95: percentile(durations, 0.95), max: Math.max(...durations) },
|
|
115
|
+
inputChars: values.reduce((sum, item) => sum + item.inputChars, 0),
|
|
116
|
+
outputChars: values.reduce((sum, item) => sum + item.outputChars, 0),
|
|
117
|
+
inputBytes: values.reduce((sum, item) => sum + item.inputBytes, 0),
|
|
118
|
+
outputBytes: values.reduce((sum, item) => sum + item.outputBytes, 0),
|
|
119
|
+
estimatedInputTokens: values.reduce((sum, item) => sum + item.estimatedInputTokens, 0),
|
|
120
|
+
estimatedOutputTokens: values.reduce((sum, item) => sum + item.estimatedOutputTokens, 0),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return byOperation;
|
|
124
|
+
}
|
|
125
|
+
export async function buildUsageReport(startPath, options = {}) {
|
|
126
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
127
|
+
const window = windowOf(options);
|
|
128
|
+
let events = [];
|
|
129
|
+
let journalVerified = true;
|
|
130
|
+
try {
|
|
131
|
+
events = await readJournal(projectRoot);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!(error instanceof Error && /ENOENT|manifest|not initialized/i.test(error.message)))
|
|
135
|
+
throw error;
|
|
136
|
+
journalVerified = false;
|
|
137
|
+
}
|
|
138
|
+
const scoped = events.filter((event) => inWindow(event, window));
|
|
139
|
+
const usageEvents = scoped.map((event) => ({ event, payload: usagePayload(event) })).filter((item) => item.payload !== null);
|
|
140
|
+
const knowledge = await listKnowledge(projectRoot, true);
|
|
141
|
+
const deadends = await listDeadends(projectRoot, true);
|
|
142
|
+
const notes = await listNotes(projectRoot, { limit: Number.MAX_SAFE_INTEGER, maxChars: 0 });
|
|
143
|
+
const contextData = await contexts(projectRoot);
|
|
144
|
+
const used = new Map();
|
|
145
|
+
for (const event of events.filter((item) => item.type === 'record.used')) {
|
|
146
|
+
const payload = event.payload;
|
|
147
|
+
if (typeof payload.entity === 'string' && typeof payload.entityId === 'string')
|
|
148
|
+
used.set(`${payload.entity}:${payload.entityId}`, (used.get(`${payload.entity}:${payload.entityId}`) ?? 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
const verified = [...knowledge.filter((item) => item.status === 'verified').map((item) => `knowledge:${item.knowledgeId}`), ...deadends.filter((item) => item.status === 'verified').map((item) => `deadend:${item.deadendId}`)];
|
|
151
|
+
const usedVerified = verified.filter((key) => (used.get(key) ?? 0) > 0).length;
|
|
152
|
+
const recordUsed = scoped.filter((event) => event.type === 'record.used').length;
|
|
153
|
+
const lifecycle = (type) => scoped.filter((event) => event.type === type).length;
|
|
154
|
+
const buckets = new Map();
|
|
155
|
+
for (const event of scoped) {
|
|
156
|
+
const key = dayStart(event.occurredAt, window.granularity);
|
|
157
|
+
const bucket = buckets.get(key) ?? { operations: 0, usageOperations: 0, recordsUsed: 0, outputChars: 0, estimatedOutputTokens: 0, knowledgeCreated: 0, knowledgeVerified: 0, deadendsCreated: 0, deadendsVerified: 0 };
|
|
158
|
+
const payload = usagePayload(event);
|
|
159
|
+
if (payload) {
|
|
160
|
+
bucket.operations += 1;
|
|
161
|
+
bucket.usageOperations += 1;
|
|
162
|
+
bucket.outputChars += payload.outputChars;
|
|
163
|
+
bucket.estimatedOutputTokens += payload.estimatedOutputTokens;
|
|
164
|
+
}
|
|
165
|
+
if (event.type === 'record.used')
|
|
166
|
+
bucket.recordsUsed += 1;
|
|
167
|
+
if (event.type === 'knowledge.proposed')
|
|
168
|
+
bucket.knowledgeCreated += 1;
|
|
169
|
+
if (event.type === 'knowledge.verified')
|
|
170
|
+
bucket.knowledgeVerified += 1;
|
|
171
|
+
if (event.type === 'deadend.proposed')
|
|
172
|
+
bucket.deadendsCreated += 1;
|
|
173
|
+
if (event.type === 'deadend.verified')
|
|
174
|
+
bucket.deadendsVerified += 1;
|
|
175
|
+
buckets.set(key, bucket);
|
|
176
|
+
}
|
|
177
|
+
const ctxTokens = usageEvents.reduce((sum, item) => sum + item.payload.estimatedOutputTokens, 0);
|
|
178
|
+
const baseline = options.baselineTokens ?? null;
|
|
179
|
+
const savings = baseline === null ? null : baseline - ctxTokens;
|
|
180
|
+
const investment = metric(ctxTokens, 'estimated', 'tokens', 'chars-div-four');
|
|
181
|
+
const rawAssets = verified.length;
|
|
182
|
+
const usedCount = verified.reduce((sum, key) => sum + Math.min(used.get(key) ?? 0, 3), 0);
|
|
183
|
+
const trendEntries = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
184
|
+
let cumulativeInvestmentTokens = 0;
|
|
185
|
+
let cumulativeOutputChars = 0;
|
|
186
|
+
let paybackPeriodStart = null;
|
|
187
|
+
const trends = trendEntries.map(([periodStart, values]) => {
|
|
188
|
+
cumulativeInvestmentTokens += values.estimatedOutputTokens;
|
|
189
|
+
cumulativeOutputChars += values.outputChars;
|
|
190
|
+
const cumulativeSavings = baseline === null ? null : baseline - cumulativeInvestmentTokens;
|
|
191
|
+
if (paybackPeriodStart === null && cumulativeSavings !== null && cumulativeSavings >= cumulativeInvestmentTokens)
|
|
192
|
+
paybackPeriodStart = periodStart;
|
|
193
|
+
return {
|
|
194
|
+
periodStart,
|
|
195
|
+
...values,
|
|
196
|
+
cumulativeOutputChars,
|
|
197
|
+
cumulativeEstimatedOutputTokens: cumulativeInvestmentTokens,
|
|
198
|
+
cumulativeEstimatedSavings: cumulativeSavings,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const paybackReached = paybackPeriodStart !== null;
|
|
202
|
+
const paybackPeriodDays = paybackPeriodStart === null ? null : Math.max(0, (Date.parse(paybackPeriodStart) - Date.parse(window.from)) / 86_400_000);
|
|
203
|
+
return {
|
|
204
|
+
schemaVersion: 1, generatedAt: new Date().toISOString(), projectRoot, window,
|
|
205
|
+
dataQuality: { journalVerified, journalEventCount: events.length, windowEventCount: scoped.length, usageEventCount: usageEvents.length, firstUsageAt: usageEvents[0]?.event.occurredAt ?? null, lastUsageAt: usageEvents.at(-1)?.event.occurredAt ?? null, measuredMetricCount: usageEvents.length * 7, estimatedMetricCount: usageEvents.length * 2 + (ctxTokens > 0 ? 1 : 0), unavailableMetricCount: 1 },
|
|
206
|
+
assets: {
|
|
207
|
+
knowledge: { total: knowledge.length, candidate: knowledge.filter((item) => item.status === 'candidate').length, verified: knowledge.filter((item) => item.status === 'verified').length, retired: knowledge.filter((item) => ['deprecated', 'rejected'].includes(item.status)).length, created: lifecycle('knowledge.proposed'), verifiedInWindow: lifecycle('knowledge.verified') },
|
|
208
|
+
deadends: { total: deadends.length, candidate: deadends.filter((item) => item.status === 'candidate').length, verified: deadends.filter((item) => item.status === 'verified').length, retired: deadends.filter((item) => ['obsolete', 'rejected'].includes(item.status)).length, created: lifecycle('deadend.proposed'), verifiedInWindow: lifecycle('deadend.verified') },
|
|
209
|
+
notes: { total: notes.length, createdInWindow: lifecycle('note.recorded') }, contexts: { total: contextData.items.length, active: contextData.items.filter((item) => ['active', 'stable'].includes(item.status)).length, snapshots: contextData.snapshots }, rawVerifiedAssetCount: rawAssets, effectiveAssetPoints: metric(rawAssets + usedCount * 0.25, 'derived', 'asset-points'), assumptions: { reuseBonusPerUse: 0.25, maxBonusUses: 3 },
|
|
210
|
+
},
|
|
211
|
+
reuse: { recordUsedInWindow: recordUsed, recordUsedTotal: events.filter((event) => event.type === 'record.used').length, usedVerifiedAssetCount: usedVerified, verifiedAssetReuseRate: metric(rawAssets === 0 ? null : usedVerified / rawAssets, 'derived', 'ratio') },
|
|
212
|
+
investment: { contextOutputTokens: investment, providerTokens: metric(null, 'unavailable', 'tokens') },
|
|
213
|
+
payback: {
|
|
214
|
+
baselineTokens: baseline,
|
|
215
|
+
baselineSource: options.baselineSource ?? null,
|
|
216
|
+
estimatedSavings: savings === null ? metric(null, 'unavailable', 'tokens') : metric(savings, 'estimated', 'tokens'),
|
|
217
|
+
estimatedROI: baseline === null || ctxTokens === 0 ? null : savings / ctxTokens,
|
|
218
|
+
status: baseline === null ? 'no_baseline' : paybackReached ? 'reached' : 'not_reached',
|
|
219
|
+
paybackAt: paybackPeriodStart,
|
|
220
|
+
paybackPeriodDays,
|
|
221
|
+
},
|
|
222
|
+
operations: { available: usageEvents.length > 0, byOperation: operationStats(scoped), reason: usageEvents.length > 0 ? null : 'operation output telemetry is not yet recorded' },
|
|
223
|
+
trends,
|
|
224
|
+
warnings: [...(!journalVerified ? ['v2 journal unavailable; lifecycle metrics may be incomplete'] : []), 'Provider token usage is unavailable', ...(usageEvents.length === 0 ? ['Token investment is unavailable until operation output telemetry is recorded'] : [])],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function formatUsageReport(report) {
|
|
228
|
+
const assets = report.assets;
|
|
229
|
+
const payback = report.payback;
|
|
230
|
+
const lines = ['fluffy-context usage report', `Window: ${String(report.window.from).slice(0, 10)} to ${String(report.window.to).slice(0, 10)}`, '', 'ASSET VALUE', ` Verified knowledge ${assets.knowledge.verified}`, ` Verified deadends ${assets.deadends.verified}`, ` Used verified assets ${report.reuse.usedVerifiedAssetCount}`, ` Asset points ${assets.effectiveAssetPoints.value.toFixed(2)} derived`, '', 'TOKEN INVESTMENT', ` Estimated tokens ${report.investment.contextOutputTokens.value} estimated`, ' Provider tokens n/a unavailable', '', 'RETURN', ` Baseline tokens ${payback.baselineTokens ?? 'n/a'}`, ` Estimated savings ${payback.estimatedSavings.value ?? 'n/a'}`, ` ROI ${payback.estimatedROI ?? 'n/a'}`, ` Payback ${payback.status}`, '', 'COMMAND OUTPUT TREND', ` Usage events ${report.dataQuality.usageEventCount}`, '', 'DATA QUALITY', ` Journal verified ${report.dataQuality.journalVerified ? 'yes' : 'no'}`, ` Window events ${report.dataQuality.windowEventCount}`, ` Warnings ${report.warnings.length}`];
|
|
231
|
+
return `${lines.join('\n')}\n`;
|
|
232
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CognitionEventSource, UsageOperationCompletedPayload } from './types.js';
|
|
2
|
+
export type UsageOperation = UsageOperationCompletedPayload['operation'];
|
|
3
|
+
export type UsageInterface = UsageOperationCompletedPayload['interface'];
|
|
4
|
+
export declare function recordUsageOperation(startPath: string | undefined, operation: UsageOperation, source: CognitionEventSource, input: unknown, output: unknown, startedAt: number, success: boolean, error?: unknown, interfaceName?: UsageInterface): Promise<void>;
|
|
5
|
+
export declare function withUsageTelemetry<T>(startPath: string | undefined, operation: UsageOperation, source: CognitionEventSource, input: unknown, action: () => Promise<T>, interfaceName?: UsageInterface): Promise<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { appendEvent } from './journal.js';
|
|
2
|
+
function errorClass(error) {
|
|
3
|
+
return error instanceof Error ? error.constructor.name : typeof error;
|
|
4
|
+
}
|
|
5
|
+
function interfaceFor(source) {
|
|
6
|
+
return source === 'mcp' ? 'mcp' : source === 'claude-hook' ? 'hook' : source === 'agent' ? 'agent' : 'cli';
|
|
7
|
+
}
|
|
8
|
+
export async function recordUsageOperation(startPath, operation, source, input, output, startedAt, success, error, interfaceName = interfaceFor(source)) {
|
|
9
|
+
const inputText = typeof input === 'string' ? input : JSON.stringify(input ?? null);
|
|
10
|
+
const outputText = typeof output === 'string' ? output : JSON.stringify(output ?? null);
|
|
11
|
+
const payload = {
|
|
12
|
+
operation,
|
|
13
|
+
interface: interfaceName,
|
|
14
|
+
success,
|
|
15
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
16
|
+
inputChars: inputText.length,
|
|
17
|
+
outputChars: outputText.length,
|
|
18
|
+
inputBytes: Buffer.byteLength(inputText),
|
|
19
|
+
outputBytes: Buffer.byteLength(outputText),
|
|
20
|
+
estimatedInputTokens: Math.ceil(inputText.length / 4),
|
|
21
|
+
estimatedOutputTokens: Math.ceil(outputText.length / 4),
|
|
22
|
+
estimatedMethod: 'chars-div-four',
|
|
23
|
+
...(success ? {} : { errorClass: errorClass(error) }),
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
await appendEvent(startPath, {
|
|
27
|
+
type: 'usage.operation.completed',
|
|
28
|
+
source: { kind: source },
|
|
29
|
+
idempotencyKey: `usage:${operation}:${source}:${startedAt}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
30
|
+
payload,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Observability must not affect the operation being observed.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function withUsageTelemetry(startPath, operation, source, input, action, interfaceName = interfaceFor(source)) {
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await action();
|
|
41
|
+
await recordUsageOperation(startPath, operation, source, input, result, startedAt, true, undefined, interfaceName);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await recordUsageOperation(startPath, operation, source, input, { error: errorClass(error) }, startedAt, false, error, interfaceName);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -321,7 +321,7 @@ export async function compileContext(startPath, options = {}) {
|
|
|
321
321
|
});
|
|
322
322
|
return {
|
|
323
323
|
schemaVersion: 1,
|
|
324
|
-
compilerVersion: '0.7.
|
|
324
|
+
compilerVersion: '0.7.7',
|
|
325
325
|
rankingVersion: 'compiler-ranking-v2',
|
|
326
326
|
status: orientation.status,
|
|
327
327
|
projectRoot: sources.anchor.projectRoot,
|
|
@@ -16,7 +16,7 @@ export function estimateTokens(value) {
|
|
|
16
16
|
export function rebuildManifestHash(manifest) {
|
|
17
17
|
return contentHash({
|
|
18
18
|
schemaVersion: 1,
|
|
19
|
-
compilerVersion: '0.7.
|
|
19
|
+
compilerVersion: '0.7.7',
|
|
20
20
|
rankingVersion: 'compiler-ranking-v2',
|
|
21
21
|
status: manifest.status,
|
|
22
22
|
projectRoot: manifest.projectRoot,
|
|
@@ -93,7 +93,7 @@ export async function loadCompilerSources(startPath, options) {
|
|
|
93
93
|
knowledgeLimit: options.knowledgeLimit ?? 10,
|
|
94
94
|
deadendLimit: options.deadendLimit ?? 10,
|
|
95
95
|
noteLimit: options.noteLimit ?? 20,
|
|
96
|
-
});
|
|
96
|
+
}, null);
|
|
97
97
|
const projectRoot = orientation.projectRoot;
|
|
98
98
|
const git = await readGitWorktreeState(projectRoot);
|
|
99
99
|
let events = [];
|
|
@@ -201,7 +201,7 @@ export interface ContextExpandResult {
|
|
|
201
201
|
}
|
|
202
202
|
export interface ContextManifest {
|
|
203
203
|
schemaVersion: 1;
|
|
204
|
-
compilerVersion: '0.7.
|
|
204
|
+
compilerVersion: '0.7.7';
|
|
205
205
|
rankingVersion: 'compiler-ranking-v2';
|
|
206
206
|
status: 'ready' | 'no_context';
|
|
207
207
|
projectRoot: string;
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { compileContext } from '../compiler/compile.js';
|
|
4
3
|
import { addNote, listNotes } from '../runtime/notes.js';
|
|
5
|
-
import { contextExpand, contextOrient, loadContext, saveContext } from '../agent/api.js';
|
|
4
|
+
import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext, saveContext } from '../agent/api.js';
|
|
6
5
|
import { ensureRuntimeForSession } from '../cognition/runtime.js';
|
|
7
6
|
import { VERSION } from '../version.js';
|
|
8
7
|
const orientInput = z.object({
|
|
@@ -84,6 +83,16 @@ const checkpointInput = z.object({
|
|
|
84
83
|
risks: z.array(z.string()).optional(),
|
|
85
84
|
relatedFiles: z.array(z.string()).optional(),
|
|
86
85
|
});
|
|
86
|
+
const usageReportInput = z.object({
|
|
87
|
+
path: z.string().optional(),
|
|
88
|
+
since: z.string().optional(),
|
|
89
|
+
until: z.string().optional(),
|
|
90
|
+
days: z.int().nonnegative().optional(),
|
|
91
|
+
month: z.string().optional(),
|
|
92
|
+
granularity: z.enum(['day', 'week', 'month']).optional(),
|
|
93
|
+
baselineTokens: z.number().nonnegative().optional(),
|
|
94
|
+
baselineSource: z.enum(['manual', 'recorded-control']).optional(),
|
|
95
|
+
});
|
|
87
96
|
function errorMessage(error) {
|
|
88
97
|
return error instanceof Error ? error.message : String(error);
|
|
89
98
|
}
|
|
@@ -108,7 +117,7 @@ export function createMcpServer() {
|
|
|
108
117
|
}, async ({ path, ...options }) => {
|
|
109
118
|
try {
|
|
110
119
|
await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
|
|
111
|
-
return successResponse(await contextOrient(path, options));
|
|
120
|
+
return successResponse(await contextOrient(path, options, 'mcp'));
|
|
112
121
|
}
|
|
113
122
|
catch (error) {
|
|
114
123
|
return failureResponse('context_orient', error);
|
|
@@ -121,7 +130,7 @@ export function createMcpServer() {
|
|
|
121
130
|
}, async ({ path, ...input }) => {
|
|
122
131
|
try {
|
|
123
132
|
await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
|
|
124
|
-
return successResponse(await contextExpand(path, input));
|
|
133
|
+
return successResponse(await contextExpand(path, input, 'mcp'));
|
|
125
134
|
}
|
|
126
135
|
catch (error) {
|
|
127
136
|
return failureResponse('context_expand', error);
|
|
@@ -134,7 +143,7 @@ export function createMcpServer() {
|
|
|
134
143
|
}, async ({ path, ...options }) => {
|
|
135
144
|
try {
|
|
136
145
|
await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
|
|
137
|
-
return successResponse(await
|
|
146
|
+
return successResponse(await contextCompile(path, options, 'mcp'));
|
|
138
147
|
}
|
|
139
148
|
catch (error) {
|
|
140
149
|
return failureResponse('context_compile', error);
|
|
@@ -147,7 +156,7 @@ export function createMcpServer() {
|
|
|
147
156
|
}, async ({ path, ...options }) => {
|
|
148
157
|
try {
|
|
149
158
|
await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
|
|
150
|
-
return successResponse(await loadContext(path, options));
|
|
159
|
+
return successResponse(await loadContext(path, options, 'mcp'));
|
|
151
160
|
}
|
|
152
161
|
catch (error) {
|
|
153
162
|
return failureResponse('context_resume', error);
|
|
@@ -179,6 +188,18 @@ export function createMcpServer() {
|
|
|
179
188
|
return failureResponse('context_note_add', error);
|
|
180
189
|
}
|
|
181
190
|
});
|
|
191
|
+
server.registerTool('context_usage_report', {
|
|
192
|
+
title: 'Context usage report',
|
|
193
|
+
description: 'Build a read-only report of context assets, explicit reuse, estimated token investment, and operation metrics.',
|
|
194
|
+
inputSchema: usageReportInput,
|
|
195
|
+
}, async ({ path, ...options }) => {
|
|
196
|
+
try {
|
|
197
|
+
return successResponse(await contextUsageReport(path, options));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return failureResponse('context_usage_report', error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
182
203
|
server.registerTool('context_checkpoint', {
|
|
183
204
|
title: 'Checkpoint context',
|
|
184
205
|
description: 'Explicitly save context progress and create a Snapshot when changes are present; runtime rate limits remain enforced.',
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.7.
|
|
1
|
+
export declare const VERSION = "0.7.7";
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.7.
|
|
1
|
+
export const VERSION = '0.7.7';
|
package/package.json
CHANGED
|
@@ -19,12 +19,16 @@ compatibility: 需要 Node.js >=20.19.0;Git 可选。CLI 通过 npm 全局安
|
|
|
19
19
|
- 仅需要完整详情时再运行 `ctx resume --max-chars <预算>`;它会保留既有的 `lastUsedAt` 更新语义。
|
|
20
20
|
- Agent 集成从 `fluffy-context` 或 `fluffy-context/agent` 导入 `contextOrient`、`compileContext`、`saveContext`、`loadContext` 和 `searchContext`,不应直接读写 `.context` 或导入内部 `dist/...` 路径。
|
|
21
21
|
- `ctx compile` 的 `activity` section 只表示 Runtime 观察到的近期文件 metadata 变化;它不代表文件被读取或理解,也不替代 checkpoint、Note 或已验证 Knowledge。
|
|
22
|
-
- 0.7.
|
|
22
|
+
- 0.7.7 的 Compiler Manifest 提供稳定 `identity.manifestHash`、候选 `itemHash`、`tokenEstimate` 和 `level` 契约;默认输出 `summary` 层。
|
|
23
23
|
- 只对 selected candidate 使用 `ctx expand --manifest-hash ... --candidate-id ... --level structured|evidence`。必须带上原始 compile 参数和相同的 compile budget;Manifest 过期时应重新 compile,不要绕过 hash 校验。
|
|
24
24
|
- `expand` 是无状态、只读的渐进式展开:`complete` 才提供完整 `content`,`partial` 提供有界 `text` 且 `content` 为 null。它不写入 Context/journal、不记录 feedback、不读取项目文件正文、diff、命令输出或敏感数据。Agent API 使用 `contextExpand`,MCP 使用 `context_expand`。
|
|
25
25
|
- 新任务先发现已验证共识,再开始实现;候选项只在显式审查时使用。
|
|
26
26
|
- 普通查询只使用已验证 Knowledge 和 Deadend;需要审查候选项时显式使用 `--all`。
|
|
27
27
|
- CLI 业务命令的标准输出是 JSON;错误写入标准错误并返回非零退出码。解析输出时不要把 `--help` 的纯文本当作 JSON。
|
|
28
|
+
- 每周或每月复盘时使用 `ctx usage report`,不要为每条 prompt 运行;它只读 journal 和 v1 数据,不创建事件或修改项目状态。
|
|
29
|
+
- Report 中的 `chars / 4` 是明确标注的 estimated token,不是真实 Provider token;Provider token 和计费金额当前 unavailable。
|
|
30
|
+
- 只有显式 `record.used` 才算 Knowledge/Deadend reuse;compile 命中或 orient 返回不等于复用。
|
|
31
|
+
- 没有显式 baseline 时,report 不生成 savings、ROI 或 payback 数字;report 不能替代 `doctor`、`journal verify` 或人工治理。
|
|
28
32
|
|
|
29
33
|
## 快速工作流
|
|
30
34
|
|
|
@@ -46,6 +50,21 @@ ctx learn ... / ctx deadend ... → 显式 verify
|
|
|
46
50
|
|
|
47
51
|
`ctx orient` 不带查询时也有效,用于恢复当前任务和 open Note。带查询时只发现已验证的 Knowledge 与 Deadend;不要以候选项驱动普通实现。
|
|
48
52
|
|
|
53
|
+
### Usage report
|
|
54
|
+
|
|
55
|
+
在阶段性复盘、周报或月报时运行只读观测报告:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ctx usage report --days 30
|
|
59
|
+
ctx usage report --month 2026-08 --granularity week
|
|
60
|
+
ctx usage report --since 2026-08-01T00:00:00.000Z --until 2026-09-01T00:00:00.000Z --baseline-tokens 12000 --baseline-source manual
|
|
61
|
+
ctx usage report --format ascii
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
JSON 是默认输出;`--format ascii` 仅用于终端展示。报告分别统计 v1 文件资产和 v2 journal 事件,展示 Knowledge、Deadend、Note、Context、Snapshot、显式 reuse、操作趋势与 estimated token 投入。`record.used` 是唯一的 reuse 信号,候选项命中、compile 选择和普通检索都不算复用。
|
|
65
|
+
|
|
66
|
+
`--baseline-tokens` 必须是显式对照输入。未提供 baseline 时 savings、ROI 和 payback 为 `null` 或 `no_baseline`;字符换算不代表 Provider 真实 token,也不提供默认单价。报告失败时应使用 `ctx doctor` 与 `ctx journal verify` 进一步诊断,而不是把 report 当作数据修复工具。
|
|
67
|
+
|
|
49
68
|
### 初始化
|
|
50
69
|
|
|
51
70
|
```bash
|
|
@@ -103,7 +122,7 @@ ctx resume --path path/to/project --max-chars 2000
|
|
|
103
122
|
|
|
104
123
|
## MCP 与 Claude Code 集成
|
|
105
124
|
|
|
106
|
-
`ctx agent serve` 提供 MCP stdio server,注册以下工具:`context_orient`、`context_expand`、`context_compile`、`context_resume`、`context_note_list`、`context_note_add` 和 `context_checkpoint`。标准输入和输出都是 MCP 协议,不能输出提示、日志或交互文本;调用错误是单次工具错误,不应让 server 退出。
|
|
125
|
+
`ctx agent serve` 提供 MCP stdio server,注册以下工具:`context_orient`、`context_expand`、`context_compile`、`context_resume`、`context_note_list`、`context_note_add`、`context_usage_report` 和 `context_checkpoint`。标准输入和输出都是 MCP 协议,不能输出提示、日志或交互文本;调用错误是单次工具错误,不应让 server 退出。
|
|
107
126
|
|
|
108
127
|
读取工作流通常是 `context_orient → context_compile → context_expand`。`context_compile` 与 `context_expand` 继续保持有界、确定性和不读取文件正文的边界;`context_resume` 复用 `loadContext`,可能更新 `lastUsedAt`。`context_note_list` 是只读的,`context_note_add` 和 `context_checkpoint` 是显式写入操作,分别新增 Note 或保存 Snapshot;它们不会自动记录 feedback、Knowledge、Deadend 或重复 checkpoint。Knowledge/Deadend 的完整记录和治理仍使用 CLI/Agent API。
|
|
109
128
|
|
|
@@ -341,7 +360,8 @@ ctx learn "订单取消后不能再次进入支付中状态"
|
|
|
341
360
|
[ ] .contextignored 排除敏感和越界路径
|
|
342
361
|
[ ] Knowledge candidate → verified
|
|
343
362
|
[ ] Deadend candidate → verified
|
|
344
|
-
[ ] MCP context_orient/context_expand/context_compile/context_resume/context_note_list/context_note_add/context_checkpoint 可调用,且单次错误不会终止 server
|
|
363
|
+
[ ] MCP context_orient/context_expand/context_compile/context_resume/context_note_list/context_note_add/context_usage_report/context_checkpoint 可调用,且单次错误不会终止 server
|
|
364
|
+
[ ] ctx usage report 的 JSON/ASCII 输出、baseline 边界和只读性符合预期
|
|
345
365
|
[ ] MCP 只读工具不写入 Context;Note add 和 checkpoint 仅在显式调用时写入
|
|
346
366
|
[ ] Claude 集成预览不写文件,--apply 幂等且拒绝冲突
|
|
347
367
|
[ ] Hook 失败时 fail open,且不会自动 checkpoint
|