@kentwynn/kgraph 0.2.35 → 0.2.37
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/dist/cli/commands/history.d.ts +1 -1
- package/dist/cli/commands/history.js +68 -24
- package/dist/cli/commands/pack.js +22 -6
- package/dist/cli/commands/session.js +25 -4
- package/dist/cli/commands/visualize.js +26 -3
- package/dist/config/config.js +1 -0
- package/dist/context/context-pack.js +17 -0
- package/dist/context/context-query.js +14 -6
- package/dist/integrations/adapters/claude-code.js +90 -1
- package/dist/integrations/agent-skills.js +1 -1
- package/dist/integrations/instruction-blocks.js +2 -2
- package/dist/integrations/integration-registry.d.ts +9 -0
- package/dist/integrations/integration-store.js +42 -0
- package/dist/integrations/workflow-steps.js +9 -5
- package/dist/session/session-store.d.ts +2 -0
- package/dist/session/session-store.js +10 -0
- package/dist/types/session.d.ts +5 -0
- package/dist/visualization/html-template.d.ts +16 -1
- package/dist/visualization/html-template.js +120 -2
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ export interface HistoryEntry {
|
|
|
8
8
|
author?: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function registerHistoryCommand(program: Command): void;
|
|
11
|
-
export declare function readHistoryEntries(processedPath: string, rootPath: string): Promise<HistoryEntry[]>;
|
|
11
|
+
export declare function readHistoryEntries(processedPath: string, rootPath: string, cognitionPath?: string): Promise<HistoryEntry[]>;
|
|
12
12
|
/**
|
|
13
13
|
* Parses the UTC timestamp embedded in a processed interaction filename.
|
|
14
14
|
* Filename format: 2026-05-09T09-36-06-247Z-slug.md
|
|
@@ -3,8 +3,8 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { readFile, readdir } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { assertWorkspace, pathExists } from '../../storage/kgraph-paths.js';
|
|
7
6
|
import { rankByFields } from '../../context/ranking.js';
|
|
7
|
+
import { assertWorkspace, pathExists } from '../../storage/kgraph-paths.js';
|
|
8
8
|
import { KGraphError, runCommand } from '../errors.js';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
export function registerHistoryCommand(program) {
|
|
@@ -15,7 +15,7 @@ export function registerHistoryCommand(program) {
|
|
|
15
15
|
.option('--json', 'Print JSON output')
|
|
16
16
|
.action((queryParts = [], options) => runCommand(async () => {
|
|
17
17
|
const workspace = await assertWorkspace(process.cwd());
|
|
18
|
-
const entries = await readHistoryEntries(workspace.processedInteractionsPath, workspace.rootPath);
|
|
18
|
+
const entries = await readHistoryEntries(workspace.processedInteractionsPath, workspace.rootPath, workspace.cognitionPath);
|
|
19
19
|
const query = queryParts.join(' ').trim();
|
|
20
20
|
const limit = options.last !== undefined ? parseInt(options.last, 10) : 0;
|
|
21
21
|
if (options.last !== undefined && (isNaN(limit) || limit < 1)) {
|
|
@@ -44,29 +44,73 @@ export function registerHistoryCommand(program) {
|
|
|
44
44
|
}
|
|
45
45
|
}));
|
|
46
46
|
}
|
|
47
|
-
export async function readHistoryEntries(processedPath, rootPath) {
|
|
48
|
-
if (!(await pathExists(processedPath))) {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
const dirents = await readdir(processedPath, { withFileTypes: true });
|
|
52
|
-
const filenames = dirents
|
|
53
|
-
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
54
|
-
.map((e) => e.name)
|
|
55
|
-
.sort(); // ISO-prefixed filenames sort chronologically
|
|
47
|
+
export async function readHistoryEntries(processedPath, rootPath, cognitionPath) {
|
|
56
48
|
const entries = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
// Read inbox-processed interactions (raw notes archived from inbox by `kgraph update`)
|
|
50
|
+
if (await pathExists(processedPath)) {
|
|
51
|
+
const dirents = await readdir(processedPath, { withFileTypes: true });
|
|
52
|
+
const filenames = dirents
|
|
53
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
54
|
+
.map((e) => e.name)
|
|
55
|
+
.sort();
|
|
56
|
+
for (const filename of filenames) {
|
|
57
|
+
const timestamp = parseTimestampFromFilename(filename);
|
|
58
|
+
if (!timestamp)
|
|
59
|
+
continue;
|
|
60
|
+
const filePath = path.join(processedPath, filename);
|
|
61
|
+
const content = await readFile(filePath, 'utf8');
|
|
62
|
+
const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? filename;
|
|
63
|
+
const summary = content
|
|
64
|
+
.match(/^## Summary\s+([\s\S]*?)(?:\n## |\n# |$)/m)?.[1]
|
|
65
|
+
?.trim();
|
|
66
|
+
const relPath = path.relative(rootPath, filePath);
|
|
67
|
+
const author = await getGitAuthor(rootPath, relPath);
|
|
68
|
+
entries.push({
|
|
69
|
+
timestamp,
|
|
70
|
+
filename,
|
|
71
|
+
title,
|
|
72
|
+
summary,
|
|
73
|
+
text: content,
|
|
74
|
+
author,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Also include conclude/session-conclude notes from the cognition store.
|
|
79
|
+
// These bypass the inbox → update pipeline so they never land in interactions/processed/,
|
|
80
|
+
// but they are still durable knowledge events that belong in the history timeline.
|
|
81
|
+
if (cognitionPath && (await pathExists(cognitionPath))) {
|
|
82
|
+
const dirents = await readdir(cognitionPath, { withFileTypes: true });
|
|
83
|
+
const filenames = dirents
|
|
84
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
85
|
+
.map((e) => e.name)
|
|
86
|
+
.sort();
|
|
87
|
+
for (const filename of filenames) {
|
|
88
|
+
// inbox-source notes are already represented via interactions/processed/
|
|
89
|
+
const filePath = path.join(cognitionPath, filename);
|
|
90
|
+
const content = await readFile(filePath, 'utf8');
|
|
91
|
+
const source = content.match(/^Source:\s*(.+)$/m)?.[1]?.trim();
|
|
92
|
+
if (source === 'inbox')
|
|
93
|
+
continue;
|
|
94
|
+
const timestamp = parseTimestampFromFilename(filename);
|
|
95
|
+
if (!timestamp)
|
|
96
|
+
continue;
|
|
97
|
+
const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? filename;
|
|
98
|
+
const summary = content
|
|
99
|
+
.match(/^## Summary\s+([\s\S]*?)(?:\n## |\n# |$)/m)?.[1]
|
|
100
|
+
?.trim();
|
|
101
|
+
const relPath = path.relative(rootPath, filePath);
|
|
102
|
+
const author = await getGitAuthor(rootPath, relPath);
|
|
103
|
+
entries.push({
|
|
104
|
+
timestamp,
|
|
105
|
+
filename,
|
|
106
|
+
title,
|
|
107
|
+
summary,
|
|
108
|
+
text: content,
|
|
109
|
+
author,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
68
112
|
}
|
|
69
|
-
return entries;
|
|
113
|
+
return entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
70
114
|
}
|
|
71
115
|
/**
|
|
72
116
|
* Parses the UTC timestamp embedded in a processed interaction filename.
|
|
@@ -85,7 +129,7 @@ export function renderHistory(entries, useColor = supportsColor(), query = '') {
|
|
|
85
129
|
const chalk = new Chalk({ level: useColor ? 3 : 0 });
|
|
86
130
|
if (entries.length === 0) {
|
|
87
131
|
return ('\n' +
|
|
88
|
-
chalk.dim(' No
|
|
132
|
+
chalk.dim(' No cognition history found. Capture knowledge with `kgraph conclude` or write notes to .kgraph/inbox/ and run `kgraph update`.') +
|
|
89
133
|
'\n');
|
|
90
134
|
}
|
|
91
135
|
const header = ` ${chalk.bold('KGraph History')} ${chalk.dim(`· ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}${query ? ` matching "${query}"` : ''}`)}`;
|
|
@@ -28,19 +28,22 @@ export function registerPackCommand(program) {
|
|
|
28
28
|
const agent = options.agent ??
|
|
29
29
|
command.getOptionValue('agent') ??
|
|
30
30
|
findCommandOption(command, 'agent');
|
|
31
|
+
const [config, maps] = await Promise.all([
|
|
32
|
+
loadConfig(workspace),
|
|
33
|
+
readMaps(workspace),
|
|
34
|
+
]);
|
|
35
|
+
const response = await queryContext(workspace, config, maps, task);
|
|
36
|
+
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
31
37
|
if (agent) {
|
|
38
|
+
const omittedTokens = pack.omitted.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
32
39
|
await recordSessionEvent(workspace, {
|
|
33
40
|
agent: assertSessionAgent(agent),
|
|
34
41
|
type: 'context',
|
|
35
42
|
captureSource: 'automatic',
|
|
43
|
+
packUsedTokens: pack.usedTokens,
|
|
44
|
+
packOmittedTokens: omittedTokens,
|
|
36
45
|
});
|
|
37
46
|
}
|
|
38
|
-
const [config, maps] = await Promise.all([
|
|
39
|
-
loadConfig(workspace),
|
|
40
|
-
readMaps(workspace),
|
|
41
|
-
]);
|
|
42
|
-
const response = await queryContext(workspace, config, maps, task);
|
|
43
|
-
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
44
47
|
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file).split(path.sep).join('/'));
|
|
45
48
|
if (pendingInboxFiles.length > 0) {
|
|
46
49
|
pack.pendingInbox = {
|
|
@@ -104,6 +107,13 @@ export function renderPackText(pack) {
|
|
|
104
107
|
lines.push(` ◌ ${pack.omitted.length - omitted.length} more omitted items`);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
110
|
+
if (pack.warnings.length > 0) {
|
|
111
|
+
lines.push('● Warnings');
|
|
112
|
+
for (const warning of pack.warnings) {
|
|
113
|
+
lines.push(` ⚠ ${warning}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
107
117
|
lines.push('', '● Next', ' agents should consume this command with --json for the full ContextPack contract');
|
|
108
118
|
return lines.join('\n');
|
|
109
119
|
}
|
|
@@ -116,6 +126,12 @@ function appendGroup(lines, title, items) {
|
|
|
116
126
|
for (const item of items.slice(0, 6)) {
|
|
117
127
|
lines.push(` ● ${item.title} (~${item.tokenEstimate} tokens)`);
|
|
118
128
|
lines.push(` because ${formatReasons(item.reasons)}`);
|
|
129
|
+
if (item.kind === 'file') {
|
|
130
|
+
const data = item.data;
|
|
131
|
+
if (data.content !== undefined) {
|
|
132
|
+
lines.push(` content inline (~${item.tokenEstimate} tokens)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
119
135
|
if (item.kind === 'file-range') {
|
|
120
136
|
const data = item.data;
|
|
121
137
|
if (data.path && data.startLine != null && data.endLine != null) {
|
|
@@ -13,7 +13,9 @@ export function registerSessionCommand(program) {
|
|
|
13
13
|
.action((options) => runCommand(async () => {
|
|
14
14
|
const workspace = await assertWorkspace(process.cwd());
|
|
15
15
|
const report = await buildSessionReport(workspace);
|
|
16
|
-
console.log(options.json
|
|
16
|
+
console.log(options.json
|
|
17
|
+
? JSON.stringify(report, null, 2)
|
|
18
|
+
: renderSessionReport(report));
|
|
17
19
|
}));
|
|
18
20
|
session
|
|
19
21
|
.command('start')
|
|
@@ -112,10 +114,25 @@ export function renderSessionReport(report) {
|
|
|
112
114
|
lines.push(`Repeated reads: ${report.repeatedReadCount}`);
|
|
113
115
|
lines.push(`Estimated read tokens: ${report.estimatedReadTokens}`);
|
|
114
116
|
lines.push(`Estimated repeated-read tokens: ${report.estimatedRepeatedReadTokens}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('Pack Usage');
|
|
119
|
+
lines.push(` Pack calls: ${report.packCallCount}`);
|
|
120
|
+
lines.push(` Tokens used: ${report.totalPackUsedTokens}`);
|
|
121
|
+
lines.push(` Tokens filtered: ${report.totalPackOmittedTokens}`);
|
|
122
|
+
if (report.packCallCount > 0 && report.totalPackOmittedTokens > 0) {
|
|
123
|
+
const total = report.totalPackUsedTokens + report.totalPackOmittedTokens;
|
|
124
|
+
const pct = Math.round((report.totalPackOmittedTokens / total) * 100);
|
|
125
|
+
lines.push(` Filter rate: ${pct}% of candidate tokens excluded from context`);
|
|
126
|
+
}
|
|
115
127
|
lines.push('', 'Top Repeated Reads');
|
|
116
128
|
lines.push(...formatList(report.topRepeatedReads.map((item) => `- ${item.path} read ${item.count} times (~${item.estimatedTokens} tokens)`)));
|
|
117
129
|
lines.push('', 'Recent Events');
|
|
118
|
-
lines.push(...formatList(report.recentEvents.map((event) =>
|
|
130
|
+
lines.push(...formatList(report.recentEvents.map((event) => {
|
|
131
|
+
const packInfo = event.packUsedTokens !== undefined
|
|
132
|
+
? ` [used:${event.packUsedTokens} filtered:${event.packOmittedTokens ?? 0}]`
|
|
133
|
+
: '';
|
|
134
|
+
return `- ${event.agent} ${event.type}${event.path ? ` ${event.path}` : ''}${packInfo} [${event.captureSource}]`;
|
|
135
|
+
})));
|
|
119
136
|
lines.push('', 'Recent Ledger');
|
|
120
137
|
lines.push(...formatList(report.ledger.map((entry) => `- ${entry.agent} ${entry.readCount} reads, ${entry.writeCount} writes, ${entry.repeatedReadCount} repeated`)));
|
|
121
138
|
lines.push('', 'Next');
|
|
@@ -144,7 +161,9 @@ function findCommandOption(command, name) {
|
|
|
144
161
|
return undefined;
|
|
145
162
|
}
|
|
146
163
|
function normalizeSource(value) {
|
|
147
|
-
if (value === 'automatic' ||
|
|
164
|
+
if (value === 'automatic' ||
|
|
165
|
+
value === 'agent-reported' ||
|
|
166
|
+
value === 'manual') {
|
|
148
167
|
return value;
|
|
149
168
|
}
|
|
150
169
|
throw new KGraphError('--source must be automatic, agent-reported, or manual.');
|
|
@@ -153,7 +172,9 @@ function formatList(items) {
|
|
|
153
172
|
return items.length > 0 ? items : ['- None'];
|
|
154
173
|
}
|
|
155
174
|
function sessionNextActions(report) {
|
|
156
|
-
if (report.
|
|
175
|
+
if (report.packCallCount === 0 &&
|
|
176
|
+
report.readCount === 0 &&
|
|
177
|
+
report.writeCount === 0) {
|
|
157
178
|
return [
|
|
158
179
|
'- Start tracking with `kgraph session start --agent <name>`.',
|
|
159
180
|
'- Record meaningful reads/writes with `kgraph session read <path> --agent <name>` and `kgraph session write <path> --agent <name>`.',
|
|
@@ -2,10 +2,11 @@ import { exec } from 'node:child_process';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { loadConfig } from '../../config/config.js';
|
|
4
4
|
import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
|
|
5
|
+
import { readSessionState } from '../../session/session-store.js';
|
|
5
6
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
6
7
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
7
8
|
import { buildGraph } from '../../visualization/graph-builder.js';
|
|
8
|
-
import { renderHtml } from '../../visualization/html-template.js';
|
|
9
|
+
import { renderHtml, } from '../../visualization/html-template.js';
|
|
9
10
|
import { KGraphError, runCommand } from '../errors.js';
|
|
10
11
|
export function registerVisualizeCommand(program) {
|
|
11
12
|
program
|
|
@@ -22,14 +23,36 @@ export function registerVisualizeCommand(program) {
|
|
|
22
23
|
if (!(await mapsExist(workspace))) {
|
|
23
24
|
throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
|
|
24
25
|
}
|
|
25
|
-
const maps = await
|
|
26
|
+
const [maps, sessionState] = await Promise.all([
|
|
27
|
+
readMaps(workspace),
|
|
28
|
+
readSessionState(workspace),
|
|
29
|
+
]);
|
|
26
30
|
const { atoms } = await refreshKnowledgeAtomStatuses(workspace, {
|
|
27
31
|
fileMap: maps.fileMap,
|
|
28
32
|
symbolMap: maps.symbolMap,
|
|
29
33
|
});
|
|
30
34
|
await loadConfig(workspace); // ensure workspace is valid
|
|
35
|
+
const contextEvents = sessionState.events.filter((e) => e.type === 'context');
|
|
36
|
+
const sessionVizData = sessionState.events.length > 0
|
|
37
|
+
? {
|
|
38
|
+
activeAgents: Object.values(sessionState.active).map((a) => a.agent),
|
|
39
|
+
packCallCount: contextEvents.length,
|
|
40
|
+
totalPackUsedTokens: contextEvents.reduce((sum, e) => sum + (e.packUsedTokens ?? 0), 0),
|
|
41
|
+
totalPackOmittedTokens: contextEvents.reduce((sum, e) => sum + (e.packOmittedTokens ?? 0), 0),
|
|
42
|
+
readCount: sessionState.events.filter((e) => e.type === 'read')
|
|
43
|
+
.length,
|
|
44
|
+
writeCount: sessionState.events.filter((e) => e.type === 'write').length,
|
|
45
|
+
contextEvents: contextEvents.map((e) => ({
|
|
46
|
+
agent: e.agent,
|
|
47
|
+
packUsedTokens: e.packUsedTokens ?? 0,
|
|
48
|
+
packOmittedTokens: e.packOmittedTokens ?? 0,
|
|
49
|
+
timestamp: e.timestamp,
|
|
50
|
+
captureSource: e.captureSource,
|
|
51
|
+
})),
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
31
54
|
const graphData = buildGraph(maps.fileMap, maps.symbolMap, maps.dependencyMap, maps.relationshipMap, atoms);
|
|
32
|
-
const html = renderHtml(graphData, workspace.rootPath);
|
|
55
|
+
const html = renderHtml(graphData, workspace.rootPath, sessionVizData);
|
|
33
56
|
await serveGraph(html, port, options.open);
|
|
34
57
|
}));
|
|
35
58
|
}
|
package/dist/config/config.js
CHANGED
|
@@ -63,6 +63,23 @@ export function buildContextPack(response, budget, rootPath) {
|
|
|
63
63
|
omitted.push(candidate);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
// Enrich selected file items with inline content so agents have the source without
|
|
67
|
+
// a second read. This mirrors how symbol and file-range items already embed excerpts.
|
|
68
|
+
// Only files that passed the budget check are read — omitted files are left as metadata.
|
|
69
|
+
if (rootPath) {
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.kind !== 'file')
|
|
72
|
+
continue;
|
|
73
|
+
const data = item.data;
|
|
74
|
+
try {
|
|
75
|
+
const content = readFileSync(path.join(rootPath, data.path), 'utf8');
|
|
76
|
+
item.data = { ...data, content };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// best-effort — leave as metadata-only if file cannot be read
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
return {
|
|
67
84
|
task: response.query,
|
|
68
85
|
budget,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { getRecentlyCommittedFiles, getWorkingTreeChangesDetailed, isGitRepo, } from '../scanner/git-utils.js';
|
|
2
|
-
import { readDomainRecords } from '../storage/cognition-store.js';
|
|
3
|
-
import { readSessionState } from '../session/session-store.js';
|
|
4
1
|
import { atomToCognitionNote, refreshKnowledgeAtomStatuses, } from '../knowledge/atom-store.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { getCurrentCommit, getRecentlyCommittedFiles, getWorkingTreeChangesDetailed, isGitRepo, } from '../scanner/git-utils.js';
|
|
3
|
+
import { readSessionState } from '../session/session-store.js';
|
|
4
|
+
import { readDomainRecords } from '../storage/cognition-store.js';
|
|
5
|
+
import { rankByFields, tokenize } from './ranking.js';
|
|
7
6
|
export async function queryContext(workspace, config, maps, query) {
|
|
8
7
|
const refreshedAtoms = await refreshKnowledgeAtomStatuses(workspace, {
|
|
9
8
|
fileMap: maps.fileMap,
|
|
@@ -22,6 +21,7 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
22
21
|
// Collect git changes before file ranking so dirty files can influence ranking,
|
|
23
22
|
// not just appear later as a low-token pack item.
|
|
24
23
|
const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
|
|
24
|
+
const warnings = [];
|
|
25
25
|
const gitChanges = [];
|
|
26
26
|
if (await isGitRepo(workspace.rootPath)) {
|
|
27
27
|
const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
|
|
@@ -54,6 +54,14 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
54
54
|
reason: 'changed in recent commits',
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
const currentCommit = await getCurrentCommit(workspace.rootPath);
|
|
58
|
+
if (currentCommit &&
|
|
59
|
+
maps.fileMap.scannedAtCommit &&
|
|
60
|
+
currentCommit !== maps.fileMap.scannedAtCommit) {
|
|
61
|
+
const shortCurrent = currentCommit.slice(0, 7);
|
|
62
|
+
const shortScanned = maps.fileMap.scannedAtCommit.slice(0, 7);
|
|
63
|
+
warnings.push(`Structural maps were last scanned at commit ${shortScanned}; HEAD is now ${shortCurrent}. Run \`kgraph scan\` to refresh file and symbol relationships.`);
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
const gitChangedPaths = new Set(gitChanges.map((change) => change.path));
|
|
59
67
|
const relevantCognition = rankByFields(query, atoms.filter((atom) => atom.status !== 'archived'), [
|
|
@@ -263,7 +271,7 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
263
271
|
nearbySymbolExplanations,
|
|
264
272
|
gitChanges,
|
|
265
273
|
staleReferences,
|
|
266
|
-
warnings
|
|
274
|
+
warnings,
|
|
267
275
|
};
|
|
268
276
|
}
|
|
269
277
|
function applyFileRankAdjustments(ranked, context) {
|
|
@@ -6,9 +6,16 @@ export const claudeCodeAdapter = {
|
|
|
6
6
|
instructions: `## KGraph Workflow
|
|
7
7
|
|
|
8
8
|
${numberedWorkflow('claude-code', {
|
|
9
|
-
sessionQualifier: 'native hooks also report session activity
|
|
9
|
+
sessionQualifier: 'native hooks also report session activity automatically',
|
|
10
10
|
})}
|
|
11
11
|
`,
|
|
12
|
+
configFiles: [
|
|
13
|
+
{
|
|
14
|
+
path: '.claude/settings.json',
|
|
15
|
+
merge: mergeClaudeHooks,
|
|
16
|
+
remove: removeClaudeHooks,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
12
19
|
commandFiles: [
|
|
13
20
|
{
|
|
14
21
|
path: '.claude/commands/kgraph.md',
|
|
@@ -106,6 +113,88 @@ ${numberedWorkflow('claude-code')}
|
|
|
106
113
|
],
|
|
107
114
|
obsoleteCommandFiles: [],
|
|
108
115
|
};
|
|
116
|
+
const KGRAPH_HOOK_MARKER = 'kgraph-session';
|
|
117
|
+
const CLAUDE_HOOK_REGISTRATIONS = [
|
|
118
|
+
{
|
|
119
|
+
event: 'UserPromptSubmit',
|
|
120
|
+
entry: {
|
|
121
|
+
hooks: [
|
|
122
|
+
{
|
|
123
|
+
type: 'command',
|
|
124
|
+
command: 'node .claude/hooks/kgraph-session-start.cjs',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
event: 'PreToolUse',
|
|
131
|
+
entry: {
|
|
132
|
+
matcher: 'Read',
|
|
133
|
+
hooks: [
|
|
134
|
+
{
|
|
135
|
+
type: 'command',
|
|
136
|
+
command: 'node .claude/hooks/kgraph-session-pre-read.cjs',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
event: 'PostToolUse',
|
|
143
|
+
entry: {
|
|
144
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
145
|
+
hooks: [
|
|
146
|
+
{
|
|
147
|
+
type: 'command',
|
|
148
|
+
command: 'node .claude/hooks/kgraph-session-post-write.cjs',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
event: 'Stop',
|
|
155
|
+
entry: {
|
|
156
|
+
hooks: [
|
|
157
|
+
{
|
|
158
|
+
type: 'command',
|
|
159
|
+
command: 'node .claude/hooks/kgraph-session-stop.cjs',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
function mergeClaudeHooks(existing) {
|
|
166
|
+
const settings = { ...existing };
|
|
167
|
+
const hooks = typeof settings.hooks === 'object' && settings.hooks !== null
|
|
168
|
+
? { ...settings.hooks }
|
|
169
|
+
: {};
|
|
170
|
+
for (const { event, entry } of CLAUDE_HOOK_REGISTRATIONS) {
|
|
171
|
+
const entries = (hooks[event] ?? []);
|
|
172
|
+
const alreadyPresent = entries.some((e) => e.hooks.some((h) => h.command.includes(KGRAPH_HOOK_MARKER)));
|
|
173
|
+
if (!alreadyPresent) {
|
|
174
|
+
hooks[event] = [...entries, entry];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
settings.hooks = hooks;
|
|
178
|
+
return settings;
|
|
179
|
+
}
|
|
180
|
+
function removeClaudeHooks(existing) {
|
|
181
|
+
if (typeof existing.hooks !== 'object' || existing.hooks === null)
|
|
182
|
+
return existing;
|
|
183
|
+
const settings = { ...existing };
|
|
184
|
+
const hooks = { ...settings.hooks };
|
|
185
|
+
for (const event of Object.keys(hooks)) {
|
|
186
|
+
hooks[event] = hooks[event].filter((entry) => !entry.hooks.some((h) => h.command.includes(KGRAPH_HOOK_MARKER)));
|
|
187
|
+
if (hooks[event].length === 0) {
|
|
188
|
+
delete hooks[event];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(hooks).length === 0) {
|
|
192
|
+
const { hooks: _removed, ...rest } = settings;
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
195
|
+
settings.hooks = hooks;
|
|
196
|
+
return settings;
|
|
197
|
+
}
|
|
109
198
|
function hookScript(event) {
|
|
110
199
|
const pathArg = event === 'read' || event === 'write'
|
|
111
200
|
? `const payload = readPayload();
|
|
@@ -45,7 +45,7 @@ name: kgraph-pack
|
|
|
45
45
|
description: Build a budget-aware KGraph context pack
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
Run \`kgraph pack "<topic>" --budget 8000 --json --agent $AGENT\` to build a machine-readable context pack and record lightweight session context. Summarize token use, included files, symbols, relationships, git changes, session history, atoms, and omitted items with the inclusion reasons. When a symbol item includes an \`excerpt\` field, you already have the source — do not read that file
|
|
48
|
+
Run \`kgraph pack "<topic>" --budget 8000 --json --agent $AGENT\` to build a machine-readable context pack and record lightweight session context. Summarize token use, included files, symbols, relationships, git changes, session history, atoms, and omitted items with the inclusion reasons. When a symbol item includes an \`excerpt\` field or a file item includes a \`content\` field, you already have the source inline — do not read that file separately.
|
|
49
49
|
`,
|
|
50
50
|
},
|
|
51
51
|
{
|
|
@@ -36,7 +36,7 @@ export function applyContextPolicy(content, mode, agentName) {
|
|
|
36
36
|
.replaceAll(KGRAPH_CAPTURE_POLICY_PLACEHOLDER, renderCapturePolicy(agentName));
|
|
37
37
|
}
|
|
38
38
|
export function renderContextPolicy(mode, agentName) {
|
|
39
|
-
const useResultBoundary = 'Use the returned KGraph ContextPack items as the first-pass source of truth. Prefer source ranges, atoms, git changes, and inclusion reasons from the pack before broad repository search. Do not rerun the same KGraph query just to tail or reformat output, do not continue broad repository search after the target file or range is identified, do not retry malformed shell commands with broader variants, and do not run broad `find`, recursive `grep`, or repeated full-file dumps after KGraph has narrowed the target.';
|
|
39
|
+
const useResultBoundary = 'Use the returned KGraph ContextPack items as the first-pass source of truth. Prefer source ranges, atoms, git changes, and inclusion reasons from the pack before broad repository search. When a file item includes a `content` field or a symbol item includes an `excerpt` field, that IS the source — do not read the file separately. Do not rerun the same KGraph query just to tail or reformat output, do not continue broad repository search after the target file or range is identified, do not retry malformed shell commands with broader variants, and do not run broad `find`, recursive `grep`, or repeated full-file dumps after KGraph has narrowed the target.';
|
|
40
40
|
const packCommand = agentName
|
|
41
41
|
? `kgraph pack "<topic>" --budget 8000 --json --agent ${agentName}`
|
|
42
42
|
: 'kgraph pack "<topic>" --budget 8000 --json';
|
|
@@ -54,7 +54,7 @@ export function renderContextPolicy(mode, agentName) {
|
|
|
54
54
|
case 'always':
|
|
55
55
|
return `Every chat in this repository must use the correct KGraph command before answering or exploring files. ${routing} For normal repo context, code navigation, debugging, review, or edits, run \`${packCommand}\`. If that pack reports pending inbox notes, run \`${rootCommand}\` or \`kgraph update\` before relying on history or newly captured atoms. Infer the topic from the user's message. This records a lightweight KGraph session context event for the agent. ${useResultBoundary}`;
|
|
56
56
|
case 'manual':
|
|
57
|
-
return
|
|
57
|
+
return `Do not run KGraph automatically. Run \`${packCommand}\` only when the user explicitly asks for KGraph context, invokes KGraph, or needs a machine-readable repo-memory pack. If the user explicitly asks for KGraph history, inbox/update, knowledge, or doctor, run that specific KGraph command instead of pack.`;
|
|
58
58
|
case 'off':
|
|
59
59
|
return 'KGraph is disabled for this integration.';
|
|
60
60
|
case 'smart':
|
|
@@ -5,12 +5,21 @@ export interface IntegrationAdapter {
|
|
|
5
5
|
targetPath: string;
|
|
6
6
|
instructions: string;
|
|
7
7
|
commandFiles?: IntegrationCommandFile[];
|
|
8
|
+
configFiles?: IntegrationConfigFile[];
|
|
8
9
|
obsoleteCommandFiles?: string[];
|
|
9
10
|
}
|
|
10
11
|
export interface IntegrationCommandFile {
|
|
11
12
|
path: string;
|
|
12
13
|
content: string;
|
|
13
14
|
}
|
|
15
|
+
export interface IntegrationConfigFile {
|
|
16
|
+
/** Repo-relative path to a JSON config file (e.g. `.claude/settings.json`). */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Merge KGraph entries into the existing parsed JSON object and return the result. */
|
|
19
|
+
merge: (existing: Record<string, unknown>) => Record<string, unknown>;
|
|
20
|
+
/** Remove KGraph entries from the existing parsed JSON and return the result. */
|
|
21
|
+
remove: (existing: Record<string, unknown>) => Record<string, unknown>;
|
|
22
|
+
}
|
|
14
23
|
export declare function listIntegrationAdapters(): IntegrationAdapter[];
|
|
15
24
|
export declare function getIntegrationAdapter(name: string): IntegrationAdapter;
|
|
16
25
|
export declare function normalizeIntegrationNames(values: string[] | undefined): IntegrationName[];
|
|
@@ -32,6 +32,7 @@ export async function addIntegrations(workspace, names, mode = 'always') {
|
|
|
32
32
|
if (mode === 'off') {
|
|
33
33
|
await removeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name);
|
|
34
34
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.commandFiles ?? []);
|
|
35
|
+
await removeIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
35
36
|
}
|
|
36
37
|
else {
|
|
37
38
|
await writeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name, applyContextPolicy(adapter.instructions, mode, adapter.name));
|
|
@@ -43,6 +44,7 @@ export async function addIntegrations(workspace, names, mode = 'always') {
|
|
|
43
44
|
for (const file of adapter.commandFiles ?? []) {
|
|
44
45
|
writtenCommandFiles.add(file.path);
|
|
45
46
|
}
|
|
47
|
+
await upsertIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
46
48
|
}
|
|
47
49
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
|
|
48
50
|
changed.push(next);
|
|
@@ -74,6 +76,7 @@ export async function removeIntegrations(workspace, names) {
|
|
|
74
76
|
await removeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name);
|
|
75
77
|
await removeIntegrationCommandFiles(workspace.rootPath, (adapter.commandFiles ?? []).filter((file) => !retainedPaths.has(file.path)));
|
|
76
78
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
|
|
79
|
+
await removeIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
77
80
|
removed.push(adapter.name);
|
|
78
81
|
}
|
|
79
82
|
config.integrations = config.integrations.filter((integration) => !removeNames.has(integration.name));
|
|
@@ -133,3 +136,42 @@ async function pruneEmptyParents(rootPath, startDir) {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
139
|
+
async function upsertIntegrationConfigFiles(rootPath, configFiles) {
|
|
140
|
+
for (const configFile of configFiles) {
|
|
141
|
+
const fullPath = path.join(rootPath, configFile.path);
|
|
142
|
+
let existing = {};
|
|
143
|
+
if (await pathExists(fullPath)) {
|
|
144
|
+
try {
|
|
145
|
+
existing = JSON.parse(await readFile(fullPath, 'utf8'));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
existing = {};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const next = configFile.merge(existing);
|
|
152
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
153
|
+
await writeFile(fullPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function removeIntegrationConfigFiles(rootPath, configFiles) {
|
|
157
|
+
for (const configFile of configFiles) {
|
|
158
|
+
const fullPath = path.join(rootPath, configFile.path);
|
|
159
|
+
if (!(await pathExists(fullPath)))
|
|
160
|
+
continue;
|
|
161
|
+
let existing = {};
|
|
162
|
+
try {
|
|
163
|
+
existing = JSON.parse(await readFile(fullPath, 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const next = configFile.remove(existing);
|
|
169
|
+
if (Object.keys(next).length === 0) {
|
|
170
|
+
await rm(fullPath, { force: true });
|
|
171
|
+
await pruneEmptyParents(rootPath, path.dirname(fullPath));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await writeFile(fullPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -20,12 +20,16 @@ const DECISION_CONTEXT = `## Feature intent — choose based on the situation
|
|
|
20
20
|
|
|
21
21
|
**compact** exists to merge redundant knowledge. Consider it when you store something that feels overlapping with what pack already showed.
|
|
22
22
|
|
|
23
|
-
**scan** exists to refresh maps after structural changes. The root workflow handles this automatically unless you created, deleted, or renamed many files outside of kgraph commands
|
|
23
|
+
**scan** exists to refresh maps after structural changes. The root workflow handles this automatically unless you created, deleted, or renamed many files outside of kgraph commands.
|
|
24
|
+
|
|
25
|
+
**history** exists to answer "what did we do" and "when did this happen" questions. Use \`kgraph history "<topic>"\` when the user asks about prior work, decisions, or the sequence of changes — it covers the full timeline regardless of whether knowledge was captured via \`conclude\`, \`--capture\`, or inbox notes. Use \`kgraph knowledge list\` when the user wants the structured claim and evidence for an atom. Use \`kgraph pack\` when the user needs context to start or continue coding work. These three serve different intents: timeline vs. structured memory vs. coding context.
|
|
26
|
+
|
|
27
|
+
**budget** exists to control how much source context pack delivers. On large or complex projects, raise \`--budget\` (e.g. \`--budget 16000\`) when pack omits files or symbols that feel relevant — the default 8000 was tuned for small projects. When pack includes file items with a \`content\` field, the budget was already spent delivering that source inline; do not re-read those files.`;
|
|
24
28
|
const DOCTOR_STEP = `Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.`;
|
|
25
|
-
const IMPACT_STEP = `Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when
|
|
29
|
+
const IMPACT_STEP = `Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when the user asks what was done before, who made a decision, or what the sequence of changes was.`;
|
|
26
30
|
const REPAIR_STEP = `Run \`kgraph repair --dry-run\` before cleanup when stale/noisy atom refs need fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.`;
|
|
27
31
|
const COMPACT_STEP = `Run \`kgraph compact --dry-run\` when cognition looks duplicated, noisy, or stale. Run \`kgraph compact\` only when the user asks to merge/archive cognition.`;
|
|
28
|
-
const HISTORY_STEP = `Run \`kgraph history\` or \`
|
|
32
|
+
const HISTORY_STEP = `Run \`kgraph history "<topic>"\` when the user asks what was done, decided, or changed — it covers the full timeline including notes captured via \`conclude\`, \`--capture\`, and inbox. Prefer history over \`knowledge list\` when the question is about sequence, timing, or authorship rather than atom details.`;
|
|
29
33
|
const KNOWLEDGE_STEP = `Run \`kgraph knowledge list --topic "<topic>"\` or \`kgraph knowledge get <atom-id>\` when the user asks what KGraph remembers or atom provenance/lifecycle matters.`;
|
|
30
34
|
const STALE_STEP = `Run \`kgraph stale\` when changed or deleted code may have invalidated durable knowledge. Run \`kgraph blame <atom-id>\` when provenance or evidence for a memory matters.`;
|
|
31
35
|
const EXPLORATION_BOUNDARY_STEP = `Keep exploration bounded by the task. For simple edits, use KGraph to identify the likely file, then read only that file or a narrow range and make the edit. Do not keep searching after the target file is found, do not retry malformed shell commands with broader variants, and do not run broad \`find\`, recursive \`grep\`, or repeated full-file dumps after KGraph already returned candidate files.`;
|
|
@@ -45,7 +49,7 @@ function sessionStep(agentName, qualifier) {
|
|
|
45
49
|
export function numberedWorkflow(agentName, options = {}) {
|
|
46
50
|
return `1. Infer the topic from the user's request.
|
|
47
51
|
2. {{KGRAPH_CONTEXT_POLICY}}
|
|
48
|
-
3. When
|
|
52
|
+
3. When a pack item includes an \`excerpt\` field (symbol) or a \`content\` field (file), you already have the source code inline — do not read that file separately. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
49
53
|
4. ${EXPLORATION_BOUNDARY_STEP}
|
|
50
54
|
5. ${VERIFY_EDIT_STEP}
|
|
51
55
|
6. ${smartRootStep(agentName)}
|
|
@@ -71,7 +75,7 @@ ${DECISION_CONTEXT}`;
|
|
|
71
75
|
*/
|
|
72
76
|
export function bulletWorkflow(agentName, options = {}) {
|
|
73
77
|
return `- {{KGRAPH_CONTEXT_POLICY}}
|
|
74
|
-
- When
|
|
78
|
+
- When a pack item includes an \`excerpt\` field (symbol) or a \`content\` field (file), you already have the source code inline — do not read that file separately. Items in the \`omitted\` array were evaluated and excluded — do not manually search for them unless the user explicitly asks.
|
|
75
79
|
- ${EXPLORATION_BOUNDARY_STEP}
|
|
76
80
|
- ${VERIFY_EDIT_STEP}
|
|
77
81
|
- ${smartRootStep(agentName)}
|
|
@@ -11,5 +11,7 @@ export declare function recordSessionEvent(workspace: KGraphWorkspace, input: {
|
|
|
11
11
|
path?: string;
|
|
12
12
|
captureSource: SessionCaptureSource;
|
|
13
13
|
fileMap?: FileMap;
|
|
14
|
+
packUsedTokens?: number;
|
|
15
|
+
packOmittedTokens?: number;
|
|
14
16
|
}): Promise<SessionEvent>;
|
|
15
17
|
export declare function buildSessionReport(workspace: KGraphWorkspace): Promise<SessionReport>;
|
|
@@ -63,6 +63,12 @@ export async function recordSessionEvent(workspace, input) {
|
|
|
63
63
|
...(normalizedPath ? { path: normalizedPath } : {}),
|
|
64
64
|
...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
|
|
65
65
|
...(repeated !== undefined ? { repeated } : {}),
|
|
66
|
+
...(input.type === 'context' && input.packUsedTokens !== undefined
|
|
67
|
+
? { packUsedTokens: input.packUsedTokens }
|
|
68
|
+
: {}),
|
|
69
|
+
...(input.type === 'context' && input.packOmittedTokens !== undefined
|
|
70
|
+
? { packOmittedTokens: input.packOmittedTokens }
|
|
71
|
+
: {}),
|
|
66
72
|
captureSource: input.captureSource,
|
|
67
73
|
timestamp: now,
|
|
68
74
|
};
|
|
@@ -90,6 +96,7 @@ export async function buildSessionReport(workspace) {
|
|
|
90
96
|
const readEvents = state.events.filter((event) => event.type === 'read');
|
|
91
97
|
const writeEvents = state.events.filter((event) => event.type === 'write');
|
|
92
98
|
const repeatedReads = readEvents.filter((event) => event.repeated);
|
|
99
|
+
const contextEvents = state.events.filter((event) => event.type === 'context');
|
|
93
100
|
return {
|
|
94
101
|
activeAgents: Object.values(state.active),
|
|
95
102
|
readCount: readEvents.length,
|
|
@@ -97,6 +104,9 @@ export async function buildSessionReport(workspace) {
|
|
|
97
104
|
repeatedReadCount: repeatedReads.length,
|
|
98
105
|
estimatedReadTokens: sumTokens(readEvents),
|
|
99
106
|
estimatedRepeatedReadTokens: sumTokens(repeatedReads),
|
|
107
|
+
packCallCount: contextEvents.length,
|
|
108
|
+
totalPackUsedTokens: contextEvents.reduce((sum, e) => sum + (e.packUsedTokens ?? 0), 0),
|
|
109
|
+
totalPackOmittedTokens: contextEvents.reduce((sum, e) => sum + (e.packOmittedTokens ?? 0), 0),
|
|
100
110
|
topRepeatedReads: topRepeatedReads(readEvents),
|
|
101
111
|
recentEvents: state.events.slice(-10),
|
|
102
112
|
ledger: ledger.slice(-10),
|
package/dist/types/session.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SessionEvent {
|
|
|
9
9
|
path?: string;
|
|
10
10
|
tokenEstimate?: number;
|
|
11
11
|
repeated?: boolean;
|
|
12
|
+
packUsedTokens?: number;
|
|
13
|
+
packOmittedTokens?: number;
|
|
12
14
|
captureSource: SessionCaptureSource;
|
|
13
15
|
timestamp: string;
|
|
14
16
|
}
|
|
@@ -41,6 +43,9 @@ export interface SessionReport {
|
|
|
41
43
|
repeatedReadCount: number;
|
|
42
44
|
estimatedReadTokens: number;
|
|
43
45
|
estimatedRepeatedReadTokens: number;
|
|
46
|
+
packCallCount: number;
|
|
47
|
+
totalPackUsedTokens: number;
|
|
48
|
+
totalPackOmittedTokens: number;
|
|
44
49
|
topRepeatedReads: Array<{
|
|
45
50
|
path: string;
|
|
46
51
|
count: number;
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import type { GraphData } from './graph-builder.js';
|
|
2
|
-
export
|
|
2
|
+
export interface SessionVizData {
|
|
3
|
+
activeAgents: string[];
|
|
4
|
+
packCallCount: number;
|
|
5
|
+
totalPackUsedTokens: number;
|
|
6
|
+
totalPackOmittedTokens: number;
|
|
7
|
+
readCount: number;
|
|
8
|
+
writeCount: number;
|
|
9
|
+
contextEvents: Array<{
|
|
10
|
+
agent: string;
|
|
11
|
+
packUsedTokens: number;
|
|
12
|
+
packOmittedTokens: number;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
captureSource: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function renderHtml(graphData: GraphData, rootPath: string, sessionData?: SessionVizData): string;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export function renderHtml(graphData, rootPath) {
|
|
1
|
+
export function renderHtml(graphData, rootPath, sessionData) {
|
|
2
2
|
const repoName = escAttr(rootPath.split(/[\\/]/).pop() ?? 'Repository');
|
|
3
3
|
const { meta } = graphData;
|
|
4
4
|
// Prevent </script> tag injection from embedded JSON
|
|
5
|
-
const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '
|
|
5
|
+
const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '<\/script>');
|
|
6
|
+
const safeSessionData = sessionData
|
|
7
|
+
? JSON.stringify(sessionData).replace(/<\/script>/gi, '<\/script>')
|
|
8
|
+
: null;
|
|
6
9
|
return `<!DOCTYPE html>
|
|
7
10
|
<html lang="en">
|
|
8
11
|
<head>
|
|
@@ -46,6 +49,31 @@ select:hover,button:hover{background:#475569}
|
|
|
46
49
|
.li-dia{width:10px;height:10px;transform:rotate(45deg);flex-shrink:0;display:inline-block}
|
|
47
50
|
.li-sep{width:1px;height:14px;background:#334155;flex-shrink:0}
|
|
48
51
|
.li-head{font-size:11px;color:#475569;font-weight:700;letter-spacing:.04em}
|
|
52
|
+
#session-panel{width:310px;background:#1e293b;border-left:1px solid #334155;display:none;flex-direction:column;overflow:hidden;flex-shrink:0}
|
|
53
|
+
#session-panel.open{display:flex}
|
|
54
|
+
#btn-session{color:#7dd3fc;border-color:#3b82f6}
|
|
55
|
+
#btn-session.active,#btn-session:hover{background:#1e3a5f!important;color:#7dd3fc;border-color:#3b82f6}
|
|
56
|
+
#sp-head{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid #334155;gap:8px;flex-shrink:0}
|
|
57
|
+
#sp-title{font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#7dd3fc;flex-shrink:0}
|
|
58
|
+
#sp-agents{color:#475569;font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
59
|
+
#sp-close2{background:none!important;border:none!important;color:#64748b;font-size:17px;line-height:1;cursor:pointer;padding:0 2px;flex-shrink:0;box-shadow:none}
|
|
60
|
+
#sp-close2:hover{color:#e2e8f0}
|
|
61
|
+
#sp-body{padding:14px;overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:16px}
|
|
62
|
+
.sp-summary{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
|
63
|
+
.sp-metric{background:#0f172a;border:1px solid #334155;border-radius:5px;padding:8px 10px}
|
|
64
|
+
.sp-metric-val{font-size:18px;font-weight:700;color:#f1f5f9;line-height:1;font-variant-numeric:tabular-nums}
|
|
65
|
+
.sp-metric-val.accent{color:#7dd3fc}
|
|
66
|
+
.sp-metric-lbl{font-size:10px;color:#64748b;margin-top:3px;text-transform:uppercase;letter-spacing:.05em}
|
|
67
|
+
.sp-group-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#475569;padding-bottom:6px;margin-bottom:2px;border-bottom:1px solid #1e293b}
|
|
68
|
+
.sp-call-row{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid #0f172a}
|
|
69
|
+
.sp-call-row:last-child{border-bottom:none}
|
|
70
|
+
.sp-call-time{font-size:10px;color:#475569;flex-shrink:0;width:44px}
|
|
71
|
+
.sp-call-bar-wrap{flex:1;background:#0f172a;border-radius:2px;height:6px;overflow:hidden}
|
|
72
|
+
.sp-call-bar{background:#3b82f6;height:100%;border-radius:2px;min-width:2px}
|
|
73
|
+
.sp-call-toks{font-size:11px;color:#94a3b8;flex-shrink:0;width:60px;text-align:right}
|
|
74
|
+
.sp-io-row{display:flex;gap:6px}
|
|
75
|
+
.sp-io-box{flex:1;background:#0f172a;border:1px solid #334155;border-radius:5px;padding:8px 10px;text-align:center}
|
|
76
|
+
.sp-io-val{font-size:18px;font-weight:700;color:#f1f5f9}.sp-io-lbl{font-size:10px;color:#64748b;margin-top:2px;text-transform:uppercase;letter-spacing:.05em}
|
|
49
77
|
</style>
|
|
50
78
|
</head>
|
|
51
79
|
<body>
|
|
@@ -63,6 +91,7 @@ select:hover,button:hover{background:#475569}
|
|
|
63
91
|
</select>
|
|
64
92
|
<button id="btn-fit" title="Fit graph to viewport">\u229f Fit</button>
|
|
65
93
|
<button id="btn-png" title="Download as PNG">\u2193 PNG</button>
|
|
94
|
+
${sessionData ? ` <button id="btn-session" title="View session stats">\u26a1 Session</button>` : ''}
|
|
66
95
|
</div>
|
|
67
96
|
</div>
|
|
68
97
|
<div id="main">
|
|
@@ -74,6 +103,16 @@ select:hover,button:hover{background:#475569}
|
|
|
74
103
|
</div>
|
|
75
104
|
<div id="sb-body"></div>
|
|
76
105
|
</div>
|
|
106
|
+
${sessionData
|
|
107
|
+
? ` <div id="session-panel">
|
|
108
|
+
<div id="sp-head">
|
|
109
|
+
<span id="sp-title">\u26a1 Session</span>
|
|
110
|
+
<span id="sp-agents">${sessionData.activeAgents.length ? sessionData.activeAgents.join(' \xb7 ') : 'session recorded'}</span>
|
|
111
|
+
<button id="sp-close2" title="Close">\u00d7</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="sp-body"></div>
|
|
114
|
+
</div>`
|
|
115
|
+
: ''}
|
|
77
116
|
</div>
|
|
78
117
|
<div id="legend">
|
|
79
118
|
<span class="li-head">Files</span>
|
|
@@ -294,6 +333,8 @@ select:hover,button:hover{background:#475569}
|
|
|
294
333
|
document.getElementById('sb-type').textContent = d.type === 'atom' ? 'Knowledge Atom' : 'File';
|
|
295
334
|
document.getElementById('sb-body').innerHTML = d.type === 'atom' ? renderAtomPanel(d) : renderFilePanel(d);
|
|
296
335
|
document.getElementById('sidebar').classList.add('open');
|
|
336
|
+
var sp = document.getElementById('session-panel');
|
|
337
|
+
if (sp) { sp.classList.remove('open'); var sb = document.getElementById('btn-session'); if (sb) { sb.classList.remove('active'); sessionPanelOpen = false; } }
|
|
297
338
|
});
|
|
298
339
|
|
|
299
340
|
cy.on('tap', function (evt) {
|
|
@@ -337,6 +378,83 @@ select:hover,button:hover{background:#475569}
|
|
|
337
378
|
a.click();
|
|
338
379
|
document.body.removeChild(a);
|
|
339
380
|
});
|
|
381
|
+
${safeSessionData
|
|
382
|
+
? `
|
|
383
|
+
var SESSION_DATA = ${safeSessionData};
|
|
384
|
+
var sessionRendered = false;
|
|
385
|
+
var sessionPanelOpen = false;
|
|
386
|
+
|
|
387
|
+
function fmt(n) {
|
|
388
|
+
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderSessionPanel() {
|
|
392
|
+
var d = SESSION_DATA;
|
|
393
|
+
var total = d.totalPackUsedTokens + d.totalPackOmittedTokens;
|
|
394
|
+
var filterRate = total > 0 ? Math.round((d.totalPackOmittedTokens / total) * 100) : 0;
|
|
395
|
+
var avgToks = d.packCallCount > 0 ? Math.round(d.totalPackUsedTokens / d.packCallCount) : 0;
|
|
396
|
+
var filterCell = filterRate > 0
|
|
397
|
+
? '<div class="sp-metric"><div class="sp-metric-val accent">' + filterRate + '%</div><div class="sp-metric-lbl">Filtered</div></div>'
|
|
398
|
+
: '<div class="sp-metric"><div class="sp-metric-val" style="font-size:11px;color:#475569;line-height:1.4">None</div><div class="sp-metric-lbl">Filtered</div></div>';
|
|
399
|
+
var summaryHtml =
|
|
400
|
+
'<div class="sp-summary">' +
|
|
401
|
+
'<div class="sp-metric"><div class="sp-metric-val">' + d.packCallCount + '</div><div class="sp-metric-lbl">Pack calls</div></div>' +
|
|
402
|
+
'<div class="sp-metric"><div class="sp-metric-val accent">' + fmt(d.totalPackUsedTokens) + '</div><div class="sp-metric-lbl">Tokens used</div></div>' +
|
|
403
|
+
'<div class="sp-metric"><div class="sp-metric-val">' + fmt(avgToks) + '</div><div class="sp-metric-lbl">Avg / call</div></div>' +
|
|
404
|
+
filterCell +
|
|
405
|
+
'</div>';
|
|
406
|
+
var maxUsed = 0;
|
|
407
|
+
d.contextEvents.forEach(function (e) { if ((e.packUsedTokens || 0) > maxUsed) maxUsed = e.packUsedTokens || 0; });
|
|
408
|
+
var callRows = '';
|
|
409
|
+
if (d.contextEvents.length > 0) {
|
|
410
|
+
callRows = '<div><div class="sp-group-lbl">Pack calls (' + d.contextEvents.length + ')</div>';
|
|
411
|
+
d.contextEvents.forEach(function (e) {
|
|
412
|
+
var used = e.packUsedTokens || 0;
|
|
413
|
+
var filt = e.packOmittedTokens || 0;
|
|
414
|
+
var barPct = maxUsed > 0 ? Math.round(used / maxUsed * 100) : 100;
|
|
415
|
+
var ts = e.timestamp ? new Date(e.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
416
|
+
var filtLabel = filt > 0 ? ' <span style="color:#ef4444">-' + fmt(filt) + '</span>' : '';
|
|
417
|
+
callRows +=
|
|
418
|
+
'<div class="sp-call-row">' +
|
|
419
|
+
'<span class="sp-call-time">' + esc(ts) + '</span>' +
|
|
420
|
+
'<div class="sp-call-bar-wrap"><div class="sp-call-bar" style="width:' + barPct + '%"></div></div>' +
|
|
421
|
+
'<span class="sp-call-toks">' + fmt(used) + filtLabel + '</span>' +
|
|
422
|
+
'</div>';
|
|
423
|
+
});
|
|
424
|
+
callRows += '</div>';
|
|
425
|
+
}
|
|
426
|
+
var ioHtml =
|
|
427
|
+
'<div><div class="sp-group-lbl">File access</div>' +
|
|
428
|
+
'<div class="sp-io-row">' +
|
|
429
|
+
'<div class="sp-io-box"><div class="sp-io-val">' + d.readCount + '</div><div class="sp-io-lbl">Reads</div></div>' +
|
|
430
|
+
'<div class="sp-io-box"><div class="sp-io-val">' + d.writeCount + '</div><div class="sp-io-lbl">Writes</div></div>' +
|
|
431
|
+
'</div></div>';
|
|
432
|
+
document.getElementById('sp-body').innerHTML = summaryHtml + callRows + ioHtml;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
document.getElementById('btn-session').addEventListener('click', function () {
|
|
436
|
+
var panel = document.getElementById('session-panel');
|
|
437
|
+
sessionPanelOpen = !sessionPanelOpen;
|
|
438
|
+
if (sessionPanelOpen) {
|
|
439
|
+
panel.classList.add('open');
|
|
440
|
+
this.classList.add('active');
|
|
441
|
+
if (!sessionRendered) {
|
|
442
|
+
renderSessionPanel();
|
|
443
|
+
sessionRendered = true;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
panel.classList.remove('open');
|
|
447
|
+
this.classList.remove('active');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
document.getElementById('sp-close2').addEventListener('click', function () {
|
|
452
|
+
document.getElementById('session-panel').classList.remove('open');
|
|
453
|
+
document.getElementById('btn-session').classList.remove('active');
|
|
454
|
+
sessionPanelOpen = false;
|
|
455
|
+
});
|
|
456
|
+
`
|
|
457
|
+
: ''}
|
|
340
458
|
})();
|
|
341
459
|
</script>
|
|
342
460
|
</body>
|