knodin 0.8.2 → 0.8.4
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 +36 -13
- package/dist/bin/cli.js +248 -62
- package/dist/src/agent-events.js +128 -0
- package/dist/src/agent-hooks.js +156 -0
- package/dist/src/cli-model.js +16 -1
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/index.js +911 -55
- package/dist/src/init-progress-worker.js +4 -40
- package/dist/src/output-telemetry.js +8 -3
- package/dist/src/progress-worker-runtime.js +46 -0
- package/dist/src/repair-progress-worker.js +3 -40
- package/dist/src/resource-reachability.js +456 -0
- package/dist/src/response-budget.js +69 -65
- package/dist/src/session-telemetry.js +163 -0
- package/dist/src/tools/knodin-tools.js +12 -7
- package/docs/BEHAVIORAL-CONTRACT.md +47 -5
- package/docs/CLI.md +27 -0
- package/docs/COMPARISON.md +10 -0
- package/docs/DEMO.md +49 -0
- package/docs/INSTALLATION.md +11 -0
- package/docs/MCP.md +5 -0
- package/docs/TELEMETRY.md +19 -1
- package/docs/releases/0.8.3.md +47 -0
- package/docs/releases/0.8.4.md +22 -0
- package/package.json +14 -1
- package/roadmap/competitive-roadmap.md +115 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const OWNED_COMMAND = "knodin agent-event";
|
|
5
|
+
function settingsPath(homeDir = os.homedir()) {
|
|
6
|
+
return path.join(homeDir, ".claude", "settings.json");
|
|
7
|
+
}
|
|
8
|
+
function assertNotSymlink(filePath) {
|
|
9
|
+
for (const candidate of [path.dirname(filePath), filePath]) {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.lstatSync(candidate).isSymbolicLink())
|
|
12
|
+
throw new Error(`knodin agent-hooks: refusing symlinked path ${candidate}`);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code !== "ENOENT")
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readSettings(filePath) {
|
|
21
|
+
if (!fs.existsSync(filePath))
|
|
22
|
+
return {};
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`knodin agent-hooks: invalid Claude settings JSON: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
31
|
+
throw new Error("knodin agent-hooks: Claude settings must contain a JSON object");
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
function atomicWrite(filePath, content) {
|
|
35
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
36
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
37
|
+
fs.writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
38
|
+
fs.renameSync(temporary, filePath);
|
|
39
|
+
}
|
|
40
|
+
function ownedEntry(value) {
|
|
41
|
+
return JSON.stringify(value).includes(OWNED_COMMAND);
|
|
42
|
+
}
|
|
43
|
+
function removeOwned(document) {
|
|
44
|
+
const hooks = document.hooks;
|
|
45
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
46
|
+
return;
|
|
47
|
+
const hookObject = hooks;
|
|
48
|
+
for (const [event, value] of Object.entries(hookObject)) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
continue;
|
|
51
|
+
const retained = value.filter((entry) => !ownedEntry(entry));
|
|
52
|
+
if (retained.length === 0)
|
|
53
|
+
delete hookObject[event];
|
|
54
|
+
else
|
|
55
|
+
hookObject[event] = retained;
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(hookObject).length === 0)
|
|
58
|
+
delete document.hooks;
|
|
59
|
+
}
|
|
60
|
+
function hook(command, event, matcher) {
|
|
61
|
+
return {
|
|
62
|
+
...(matcher ? { matcher } : {}),
|
|
63
|
+
hooks: [
|
|
64
|
+
{
|
|
65
|
+
type: "command",
|
|
66
|
+
command: `${command} agent-event ${event}`,
|
|
67
|
+
timeout: 5,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function managedHooks(command) {
|
|
73
|
+
const observed = "mcp__knodin__knodin|Bash|Read|Glob|Grep|Edit|Write|NotebookEdit";
|
|
74
|
+
return {
|
|
75
|
+
SessionStart: [hook(command, "session-start", "startup|resume|clear|compact")],
|
|
76
|
+
UserPromptSubmit: [hook(command, "turn-start")],
|
|
77
|
+
PreToolUse: [hook(command, "pre-tool", observed)],
|
|
78
|
+
PostToolUse: [hook(command, "post-tool", observed)],
|
|
79
|
+
PostToolUseFailure: [hook(command, "tool-failure", observed)],
|
|
80
|
+
Stop: [hook(command, "turn-end")],
|
|
81
|
+
SessionEnd: [hook(command, "session-end")],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function mergedSettings(existing, command) {
|
|
85
|
+
const document = structuredClone(existing);
|
|
86
|
+
removeOwned(document);
|
|
87
|
+
const hooks = document.hooks && typeof document.hooks === "object" && !Array.isArray(document.hooks)
|
|
88
|
+
? document.hooks
|
|
89
|
+
: {};
|
|
90
|
+
for (const [event, entries] of Object.entries(managedHooks(command))) {
|
|
91
|
+
const current = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
92
|
+
hooks[event] = [...current, ...entries];
|
|
93
|
+
}
|
|
94
|
+
document.hooks = hooks;
|
|
95
|
+
return document;
|
|
96
|
+
}
|
|
97
|
+
export function inspectClaudeAgentHooks(options = {}) {
|
|
98
|
+
const filePath = settingsPath(options.homeDir);
|
|
99
|
+
assertNotSymlink(filePath);
|
|
100
|
+
const document = readSettings(filePath);
|
|
101
|
+
const serialized = JSON.stringify(document);
|
|
102
|
+
const count = serialized.split(OWNED_COMMAND).length - 1;
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
client: "claude",
|
|
106
|
+
settingsPath: filePath,
|
|
107
|
+
installed: count > 0,
|
|
108
|
+
managedEntries: count,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function installClaudeAgentHooks(options = {}) {
|
|
112
|
+
const filePath = settingsPath(options.homeDir);
|
|
113
|
+
assertNotSymlink(filePath);
|
|
114
|
+
const before = readSettings(filePath);
|
|
115
|
+
const after = mergedSettings(before, options.command ?? "knodin");
|
|
116
|
+
const beforeText = `${JSON.stringify(before, null, 2)}\n`;
|
|
117
|
+
const afterText = `${JSON.stringify(after, null, 2)}\n`;
|
|
118
|
+
const changed = beforeText !== afterText;
|
|
119
|
+
if (changed && !options.dryRun) {
|
|
120
|
+
if (fs.existsSync(filePath)) {
|
|
121
|
+
const backup = `${filePath}.knodin-backup`;
|
|
122
|
+
if (!fs.existsSync(backup))
|
|
123
|
+
fs.copyFileSync(filePath, backup, fs.constants.COPYFILE_EXCL);
|
|
124
|
+
}
|
|
125
|
+
atomicWrite(filePath, afterText);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
client: "claude",
|
|
130
|
+
settingsPath: filePath,
|
|
131
|
+
changed,
|
|
132
|
+
dryRun: options.dryRun === true,
|
|
133
|
+
managedEntries: Object.keys(managedHooks(options.command ?? "knodin")).length,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function uninstallClaudeAgentHooks(options = {}) {
|
|
137
|
+
const filePath = settingsPath(options.homeDir);
|
|
138
|
+
assertNotSymlink(filePath);
|
|
139
|
+
if (!fs.existsSync(filePath))
|
|
140
|
+
return { removed: false, settingsPath: filePath };
|
|
141
|
+
const document = readSettings(filePath);
|
|
142
|
+
const hadOwned = ownedEntry(document);
|
|
143
|
+
removeOwned(document);
|
|
144
|
+
if (hadOwned) {
|
|
145
|
+
if (Object.keys(document).length === 0)
|
|
146
|
+
fs.unlinkSync(filePath);
|
|
147
|
+
else
|
|
148
|
+
atomicWrite(filePath, `${JSON.stringify(document, null, 2)}\n`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
client: "claude",
|
|
153
|
+
removed: hadOwned,
|
|
154
|
+
settingsPath: filePath,
|
|
155
|
+
};
|
|
156
|
+
}
|
package/dist/src/cli-model.js
CHANGED
|
@@ -251,6 +251,14 @@ function createCliProgram(capture = () => { }) {
|
|
|
251
251
|
leaf(program, "serve", "run the one-tool MCP gateway on stdio", capture);
|
|
252
252
|
leaf(program, "version", "print the installed knodin version", capture);
|
|
253
253
|
leaf(program, "hook-refresh <kind> [values...]", "internal Git lifecycle refresh", capture);
|
|
254
|
+
const agentHooks = program
|
|
255
|
+
.command("agent-hooks")
|
|
256
|
+
.description("manage optional user-global coding-agent lifecycle hooks");
|
|
257
|
+
for (const action of ["install", "status", "uninstall"])
|
|
258
|
+
leaf(agentHooks, action, `${action} optional coding-agent hooks`, capture)
|
|
259
|
+
.option("--client <client>", "coding-agent client; currently claude")
|
|
260
|
+
.option("--dry-run", "preview settings changes without writing");
|
|
261
|
+
leaf(program, "agent-event <event>", "internal coding-agent lifecycle event", capture);
|
|
254
262
|
leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
|
|
255
263
|
addRepositoryCommands(program, capture);
|
|
256
264
|
addRemoteCommands(program, capture);
|
|
@@ -283,7 +291,14 @@ function createCliProgram(capture = () => { }) {
|
|
|
283
291
|
.command("telemetry")
|
|
284
292
|
.description("manage local opt-in ROI telemetry")
|
|
285
293
|
.allowExcessArguments(false)
|
|
286
|
-
.addArgument(new Argument("<action>").choices([
|
|
294
|
+
.addArgument(new Argument("<action>").choices([
|
|
295
|
+
"enable",
|
|
296
|
+
"disable",
|
|
297
|
+
"status",
|
|
298
|
+
"report",
|
|
299
|
+
"export",
|
|
300
|
+
"clear",
|
|
301
|
+
]));
|
|
287
302
|
telemetry
|
|
288
303
|
.option("--input <path>", "telemetry input path")
|
|
289
304
|
.option("--output <path>", "dashboard or evidence-bundle output path")
|
|
@@ -27,6 +27,7 @@ The \`query\` operation lets you ask precise, structured questions about your co
|
|
|
27
27
|
- **tests_for <target>**: Find test files that cover this symbol.
|
|
28
28
|
- **file_summary <target>**: Lists all symbols defined in a file.
|
|
29
29
|
- **shortest_path <from> <to>**: Find the call-graph chain connecting two symbols.
|
|
30
|
+
- **cross_substrate_path <from> <to>**: Prove one bounded static Salesforce Flow action to a uniquely resolved Apex \`@InvocableMethod\`. Returns exact XML and Apex evidence, stable endpoint identities, freshness, explicit omissions, and budget metadata; it does not resolve Terraform, dbt, or arbitrary substrate crossings.
|
|
30
31
|
- **impact <symbol>**: Directional, depth-bounded symbol reach with stable selectors, relation/confidence/test filters, edge evidence, and heuristic summaries. Use \`impactMode: "file"\` explicitly for a distinctly labeled changed-file blast radius.
|
|
31
32
|
- **dead_code**: Surface exported or unexported symbols with zero references.
|
|
32
33
|
- **large_functions / large_files**: Spot complex "god" functions and files exceeding size thresholds.
|