@vpxa/aikit 0.1.208 → 0.1.210
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/package.json +1 -1
- package/packages/cli/dist/index.js +13 -13
- package/packages/cli/dist/{init-BoKsQg2-.js → init-CYp6FjZO.js} +1 -1
- package/packages/cli/dist/{templates-D-McT4sX.js → templates-CeowBw5E.js} +1 -1
- package/packages/core/dist/index.d.ts +2 -0
- package/packages/server/dist/auth-Bz5dmZgR.js +1 -0
- package/packages/server/dist/bin.js +12 -6
- package/packages/server/dist/config-C4mVyqAF.js +2 -0
- package/packages/server/dist/config-jGZ91cRx.js +1 -0
- package/packages/server/dist/curated-manager-D1u5qOwK.js +7 -0
- package/packages/server/dist/evolution-9hXRopDC.js +3 -0
- package/packages/server/dist/evolution-DJhTM6nu.js +2 -0
- package/packages/server/dist/index.d.ts +28 -1
- package/packages/server/dist/index.js +1 -1
- package/packages/server/dist/lessons-B05P_TOl.js +3 -0
- package/packages/server/dist/lessons-D7sdHa2e.js +2 -0
- package/packages/server/dist/promotion-Bd_YB7E1.js +3 -0
- package/packages/server/dist/promotion-OY53YCsT.js +2 -0
- package/packages/server/dist/proxy.js +1 -1
- package/packages/server/dist/replay-interceptor-CGLyom5f.js +7 -0
- package/packages/server/dist/retention-B4ITAs7F.js +1 -0
- package/packages/server/dist/retention-C3tsarCT.js +2 -0
- package/packages/server/dist/rolldown-runtime-DT7IzrpZ.js +1 -0
- package/packages/server/dist/{server-BaMsrcyc.js → server-BA1mIjBc.js} +163 -139
- package/packages/server/dist/{server-BhQwVWsr.js → server-CtFr8YsZ.js} +162 -140
- package/packages/server/dist/supersession-9edUDEQ8.js +1 -0
- package/packages/server/dist/{version-check-BgHzxxCW.js → version-check-D_uN0n0Y.js} +1 -1
- package/packages/store/dist/index.js +30 -30
- package/packages/tools/dist/index.d.ts +9 -1
- package/packages/tools/dist/index.js +78 -79
- package/packages/tools/package.json +5 -5
- package/scaffold/dist/adapters/claude-code.mjs +10 -10
- package/scaffold/dist/adapters/copilot.mjs +17 -17
- package/scaffold/dist/adapters/hooks.mjs +1 -0
- package/scaffold/dist/definitions/bodies.mjs +20 -114
- package/scaffold/dist/definitions/exec-hooks.mjs +1 -0
- package/scaffold/dist/definitions/flows.mjs +81 -345
- package/scaffold/dist/definitions/protocols.mjs +21 -4
- package/scaffold/dist/definitions/skills/aikit.mjs +35 -55
- package/scaffold/dist/definitions/skills/lesson-learned.mjs +46 -0
- package/scaffold/general/hooks/scripts/_runtime.mjs +161 -0
- package/scaffold/general/hooks/scripts/post-edit-check.mjs +36 -0
- package/scaffold/general/hooks/scripts/pre-compact-save.mjs +13 -0
- package/scaffold/general/hooks/scripts/privacy-guard.mjs +39 -0
- package/scaffold/general/hooks/scripts/scout-guard.mjs +45 -0
- package/scaffold/general/hooks/scripts/session-init.mjs +85 -0
- package/scaffold/general/hooks/scripts/session-learn.mjs +53 -0
- package/scaffold/general/hooks/scripts/session-observer.mjs +77 -0
- package/scaffold/general/hooks/scripts/subagent-context.mjs +59 -0
- package/packages/server/dist/auth-BfqgawfR.js +0 -1
- package/packages/server/dist/config-DAnAxrUW.js +0 -1
- package/packages/server/dist/config-PfoXsIC3.js +0 -2
- package/packages/server/dist/curated-manager-BnP6VqvL.js +0 -7
- package/packages/server/dist/supersession-BIV-v6JG.js +0 -3
- package/packages/server/dist/supersession-DJQGXMWm.js +0 -2
|
@@ -12,7 +12,9 @@ metadata:
|
|
|
12
12
|
|
|
13
13
|
# @vpxa/aikit — AI Kit
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> This skill provides DEEP GUIDANCE beyond base instructions. For tool routing rules, see your base instructions.
|
|
16
|
+
|
|
17
|
+
Local-first AI developer toolkit with 60+ tools for search, analysis, compression, validation, memory, flows, and coordination.
|
|
16
18
|
|
|
17
19
|
## When to Use
|
|
18
20
|
|
|
@@ -45,40 +47,6 @@ If you are unsure which tool fits, ask AI Kit for the live catalog with \`list_t
|
|
|
45
47
|
4. Reuse previous context before searching again.
|
|
46
48
|
5. Use reference docs only when the main routing logic is not enough.
|
|
47
49
|
|
|
48
|
-
## Tool Selection (Decision Tree)
|
|
49
|
-
|
|
50
|
-
~~~text
|
|
51
|
-
Need to understand code?
|
|
52
|
-
├─ Just structure? → file_summary (exports, imports — ~50 tokens)
|
|
53
|
-
├─ Specific section? → compact({ path, query }) — 5-20x token reduction
|
|
54
|
-
├─ Multiple files? → digest (multi-source compression)
|
|
55
|
-
├─ Need relationships? → graph (find_nodes → neighbors)
|
|
56
|
-
└─ Need exact lines? → read_file (ONLY for editing)
|
|
57
|
-
|
|
58
|
-
Need to find something?
|
|
59
|
-
├─ Code/symbols? → search (hybrid — default)
|
|
60
|
-
├─ Symbol definition? → symbol (definition + refs + call context)
|
|
61
|
-
├─ Usage examples? → find({ mode: 'examples', query })
|
|
62
|
-
├─ Cross-file flow? → trace (forward/backward/both)
|
|
63
|
-
└─ Change impact? → blast_radius
|
|
64
|
-
|
|
65
|
-
Need to validate?
|
|
66
|
-
├─ Type errors? → check (typecheck + lint combined)
|
|
67
|
-
├─ Tests pass? → test_run (structured output)
|
|
68
|
-
└─ Full audit? → audit (structure + deps + health)
|
|
69
|
-
|
|
70
|
-
Need to remember?
|
|
71
|
-
├─ Store decision? → knowledge({ action: 'remember' })
|
|
72
|
-
├─ Find past decision? → search({ query, origin: 'curated' })
|
|
73
|
-
├─ Session state? → stash (ephemeral) or checkpoint (persistent)
|
|
74
|
-
└─ Cross-session? → knowledge (curated memory)
|
|
75
|
-
|
|
76
|
-
Need tool discovery?
|
|
77
|
-
├─ Full live catalog? → list_tools
|
|
78
|
-
├─ Know capability, not name? → search_tools
|
|
79
|
-
└─ Need details first? → describe_tool or guide
|
|
80
|
-
~~~
|
|
81
|
-
|
|
82
50
|
**When tools return empty:**
|
|
83
51
|
- \`search\` → 0 results? Broaden query terms OR fall back to \`find({ pattern })\` with regex
|
|
84
52
|
- \`symbol\` → not found? Check spelling, try \`search\` with partial name
|
|
@@ -118,7 +86,6 @@ Without session discipline, agents repeat work, miss context, and make decisions
|
|
|
118
86
|
- NEVER \`search\` then immediately \`read_file\` the result — search already returns content snippets
|
|
119
87
|
- NEVER call \`compact\` on a file you just \`file_summary\`'d — pick one retrieval depth
|
|
120
88
|
- NEVER stash >10 items without \`checkpoint\` — stash has no TTL, checkpoints do
|
|
121
|
-
- NEVER \`read_file\` a file >50 lines to "understand" it — \`file_summary\` → \`compact\` decision tree
|
|
122
89
|
- NEVER run \`reindex\` mid-implementation — wait until all edits are done
|
|
123
90
|
- NEVER skip \`search\` before implementing — past decisions may exist that constrain your approach
|
|
124
91
|
- NEVER echo full subagent output to user — compress with \`stash\` + brief summary
|
|
@@ -193,6 +160,38 @@ Capture reusable engineering insights via \`knowledge({ action: "lesson", subAct
|
|
|
193
160
|
|
|
194
161
|
See the \`lesson-learned\` skill for the full extraction workflow.
|
|
195
162
|
|
|
163
|
+
### Lesson Lifecycle Management
|
|
164
|
+
|
|
165
|
+
| SubAction | Purpose | Default |
|
|
166
|
+
|-----------|---------|---------|
|
|
167
|
+
| \`prune\` | Archive stale lessons (confidence < 15, idle > 30d) | dryRun: true |
|
|
168
|
+
| \`group\` | Cluster similar lessons, add group tags | dryRun: true |
|
|
169
|
+
| \`promote\` | Cross-workspace promotion to global store | dryRun: true |
|
|
170
|
+
| \`demote\` | Remove from global store | -- |
|
|
171
|
+
| \`auto-observe\` | Pattern detection from tool outputs | automatic |
|
|
172
|
+
|
|
173
|
+
**Maintenance workflow** (periodic):
|
|
174
|
+
\`\`\`
|
|
175
|
+
knowledge({ action: "lesson", subAction: "prune" }) // review stale
|
|
176
|
+
knowledge({ action: "lesson", subAction: "group" }) // organize
|
|
177
|
+
knowledge({ action: "lesson", subAction: "promote" }) // share universal
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
### Session Prelude
|
|
181
|
+
Request context-primed session start:
|
|
182
|
+
\`\`\`
|
|
183
|
+
status({ includePrelude: true })
|
|
184
|
+
\`\`\`
|
|
185
|
+
Returns: top 3 lessons (by decayed confidence), top 2 conventions (by recency), last session checkpoint.
|
|
186
|
+
Use at session start for immediate situational awareness.
|
|
187
|
+
|
|
188
|
+
### Confidence Decay
|
|
189
|
+
Lesson confidence decays over time via Ebbinghaus curve:
|
|
190
|
+
- Accessing lessons (via withdraw, list-lessons) implicitly reinforces them
|
|
191
|
+
- Unaccessed lessons gradually lose confidence
|
|
192
|
+
- Below threshold 15 + idle > 30 days -> eligible for pruning
|
|
193
|
+
- \`knowledge({ action: "flagged" })\` surfaces decayed entries for review
|
|
194
|
+
|
|
196
195
|
### Supersession (automatic dedup)
|
|
197
196
|
When you \`remember()\`, similar entries are detected automatically:
|
|
198
197
|
- Jaccard > 0.7 → flagged for review
|
|
@@ -216,18 +215,6 @@ Load these only when the main skill is not enough:
|
|
|
216
215
|
- \`references/forge-protocol.md\` — tiering, evidence, gates
|
|
217
216
|
- \`references/search-patterns.md\` — search, trace, graph, compression patterns
|
|
218
217
|
|
|
219
|
-
## NEVER
|
|
220
|
-
|
|
221
|
-
- NEVER use \`read_file\` to "understand" a file. \`file_summary\` gives structure in ~50 tokens instead of hundreds.
|
|
222
|
-
- NEVER use \`grep_search\` or \`semantic_search\` when \`search\` is available. \`search\` combines both strategies and ranks them.
|
|
223
|
-
- NEVER run tsc, lint, or tests through the terminal when \`check()\` and \`test_run()\` exist. Structured output beats terminal noise.
|
|
224
|
-
- NEVER skip \`status()\` at session start. If index state is unknown, every later choice is lower quality.
|
|
225
|
-
- NEVER call \`knowledge({ action: 'remember' })\` for trivial facts. Memory is for decisions, conventions, lessons, and durable findings.
|
|
226
|
-
- NEVER search the same thing twice without checking \`stash\` or prior results.
|
|
227
|
-
- NEVER use a long flat tool catalog as your primary routing aid. Use runtime discovery when you need exact tool metadata.
|
|
228
|
-
- NEVER jump to \`analyze\` for simple local questions. Start with cheaper retrieval and escalate only if needed.
|
|
229
|
-
- NEVER leave structural changes unindexed. Run \`reindex\` when symbols, files, or imports change.
|
|
230
|
-
|
|
231
218
|
## Complementary Skills
|
|
232
219
|
|
|
233
220
|
- Load \`typescript\` before TypeScript-heavy implementation work.
|
|
@@ -235,13 +222,6 @@ Load these only when the main skill is not enough:
|
|
|
235
222
|
- Load \`session-handoff\` when context is filling up or a pause is imminent.
|
|
236
223
|
- Load \`repo-access\` when repo auth fails.
|
|
237
224
|
|
|
238
|
-
## Practical Defaults
|
|
239
|
-
|
|
240
|
-
- Default search mode: \`search\` with hybrid ranking.
|
|
241
|
-
- Default read path: \`file_summary\` then \`compact\`.
|
|
242
|
-
- Default validation pair: \`check\` then \`test_run\`.
|
|
243
|
-
- Default persistence: \`knowledge remember\` for durable findings, \`stash\` for everything temporary.
|
|
244
|
-
|
|
245
225
|
## Self-Dogfooding
|
|
246
226
|
|
|
247
227
|
When developing AI Kit itself, rebuild generated output before trusting runtime behavior, and reindex after structural changes so the toolkit can see its own new shape.
|
|
@@ -272,6 +272,8 @@ knowledge({ action: "lesson", subAction: "create", context: "<what happened —
|
|
|
272
272
|
- 80-90: Pattern confirmed across multiple PRs/sessions
|
|
273
273
|
- 90+: Fundamental principle validated repeatedly -- treat as convention
|
|
274
274
|
|
|
275
|
+
Confidence decays over time based on access recency (Ebbinghaus curve). Accessing a lesson via \`withdraw\` or \`list-lessons\` implicitly reinforces it.
|
|
276
|
+
|
|
275
277
|
If you encounter a lesson that CONFIRMS a previously stored one:
|
|
276
278
|
~~~
|
|
277
279
|
knowledge({ action: "lesson", subAction: "confirm", id: "<lesson-path>" })
|
|
@@ -285,6 +287,50 @@ knowledge({ action: "lesson", subAction: "contradict", id: "<lesson-path>", reas
|
|
|
285
287
|
Before creating a new lesson, check existing ones: \`knowledge({ action: "lesson", subAction: "list-lessons" })\`
|
|
286
288
|
Only create if no existing lesson already covers this insight.
|
|
287
289
|
|
|
290
|
+
### Passive Learning (auto-observe)
|
|
291
|
+
The server automatically observes tool output patterns and can detect:
|
|
292
|
+
- Repeated error -> fix cycles (suggests lessons about common pitfalls)
|
|
293
|
+
- Consistent code patterns across files (suggests convention lessons)
|
|
294
|
+
- Recurring search queries (suggests documentation gaps)
|
|
295
|
+
|
|
296
|
+
Trigger manually: \`knowledge({ action: "lesson", subAction: "auto-observe", buffer: "<tool output>" })\`
|
|
297
|
+
Usually runs automatically via exec-hooks -- agents don't need to call this directly.
|
|
298
|
+
|
|
299
|
+
## Phase 6: Lifecycle Maintenance (Periodic)
|
|
300
|
+
|
|
301
|
+
Run these periodically (every few sessions) to maintain knowledge quality:
|
|
302
|
+
|
|
303
|
+
### Prune Stale Lessons
|
|
304
|
+
\`\`\`
|
|
305
|
+
knowledge({ action: "lesson", subAction: "prune" }) // dry-run: shows candidates
|
|
306
|
+
knowledge({ action: "lesson", subAction: "prune", dryRun: false }) // execute: archives stale lessons
|
|
307
|
+
\`\`\`
|
|
308
|
+
Archives lessons where: decayed confidence < 15 AND last accessed > 30 days ago.
|
|
309
|
+
Safety: minimum 3 lessons always retained, dry-run by default.
|
|
310
|
+
|
|
311
|
+
### Group Similar Lessons
|
|
312
|
+
\`\`\`
|
|
313
|
+
knowledge({ action: "lesson", subAction: "group" }) // dry-run: shows proposed groups
|
|
314
|
+
knowledge({ action: "lesson", subAction: "group", dryRun: false }) // execute: adds group tags
|
|
315
|
+
\`\`\`
|
|
316
|
+
Clusters lessons with similar titles (Jaccard > 0.7), adds \`group:<label>\` tags.
|
|
317
|
+
Non-destructive -- only adds tags, never modifies content.
|
|
318
|
+
|
|
319
|
+
### Cross-Project Promotion
|
|
320
|
+
\`\`\`
|
|
321
|
+
knowledge({ action: "lesson", subAction: "promote" }) // dry-run: shows candidates
|
|
322
|
+
knowledge({ action: "lesson", subAction: "promote", dryRun: false }) // execute: copies to global
|
|
323
|
+
knowledge({ action: "lesson", subAction: "demote", path: "<path>" }) // remove from global
|
|
324
|
+
\`\`\`
|
|
325
|
+
Requires user-level install. Scans all workspaces for similar lessons (Jaccard > 0.7 on insight text).
|
|
326
|
+
Promotes when: found in 2+ workspaces AND average confidence >= 80.
|
|
327
|
+
Global lessons available to ALL workspaces via \`workspaces: ["*"]\`.
|
|
328
|
+
|
|
329
|
+
### Recommended Maintenance Cadence
|
|
330
|
+
- **Every 5 sessions**: \`prune\` (dry-run review)
|
|
331
|
+
- **Every 10 sessions**: \`group\` to organize
|
|
332
|
+
- **Monthly**: \`promote\` to share universal lessons across projects
|
|
333
|
+
|
|
288
334
|
## What NOT to Do
|
|
289
335
|
|
|
290
336
|
| Avoid | Why | Instead |
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
const FILE_KEYS = new Set([
|
|
5
|
+
'dirPath',
|
|
6
|
+
'file',
|
|
7
|
+
'filePath',
|
|
8
|
+
'filePaths',
|
|
9
|
+
'files',
|
|
10
|
+
'from',
|
|
11
|
+
'path',
|
|
12
|
+
'paths',
|
|
13
|
+
'targetPath',
|
|
14
|
+
'to',
|
|
15
|
+
'uri',
|
|
16
|
+
'uris',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const escapeRegex = (value) => value.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
|
|
21
|
+
const normalizePath = (value) => {
|
|
22
|
+
if (typeof value !== 'string') return '';
|
|
23
|
+
let next = value.trim();
|
|
24
|
+
if (next.startsWith('file://')) {
|
|
25
|
+
try {
|
|
26
|
+
next = decodeURIComponent(new URL(next).pathname);
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (/^\/[a-z]:/i.test(next)) next = next.slice(1);
|
|
32
|
+
return path.normalize(next).replace(/\\/g, '/').toLowerCase();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a hook entrypoint that normalizes stdin and writes a platform-neutral response.
|
|
37
|
+
* @param {(context: ReturnType<typeof normalizeContext>) => Promise<object> | object} handler
|
|
38
|
+
*/
|
|
39
|
+
export async function createHook(handler) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await readStdin();
|
|
42
|
+
const result = (await handler(normalizeContext(raw))) ?? { decision: 'allow' };
|
|
43
|
+
respond(result);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
respond({ decision: 'deny', reason: error instanceof Error ? error.message : String(error) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads and parses the full stdin payload as JSON.
|
|
51
|
+
* @returns {Promise<object>}
|
|
52
|
+
*/
|
|
53
|
+
export function readStdin() {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
let input = '';
|
|
56
|
+
process.stdin.setEncoding('utf8');
|
|
57
|
+
process.stdin.on('data', (chunk) => {
|
|
58
|
+
input += chunk;
|
|
59
|
+
});
|
|
60
|
+
process.stdin.on('end', () => {
|
|
61
|
+
if (!input.trim()) return resolve({});
|
|
62
|
+
try {
|
|
63
|
+
resolve(JSON.parse(input));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reject(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
process.stdin.on('error', reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalizes platform-specific hook payloads into one shared shape.
|
|
74
|
+
* @param {object} raw
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeContext(raw = {}) {
|
|
77
|
+
const platform = raw.tool_name
|
|
78
|
+
? 'claude'
|
|
79
|
+
: raw.toolName
|
|
80
|
+
? 'copilot'
|
|
81
|
+
: raw.tool
|
|
82
|
+
? 'copilot-cli'
|
|
83
|
+
: 'copilot';
|
|
84
|
+
const toolInput =
|
|
85
|
+
raw.toolInput ?? raw.tool_input ?? raw.input ?? raw.arguments ?? raw.params ?? {};
|
|
86
|
+
return {
|
|
87
|
+
raw,
|
|
88
|
+
platform,
|
|
89
|
+
event: raw.event ?? raw.hookEvent ?? raw.hook_event ?? 'SessionStart',
|
|
90
|
+
toolName: raw.toolName ?? raw.tool_name ?? raw.tool ?? '',
|
|
91
|
+
toolInput,
|
|
92
|
+
filePaths: extractFilePaths(toolInput),
|
|
93
|
+
env: raw.env ?? raw.environment ?? raw.processEnv ?? {},
|
|
94
|
+
cwd: raw.cwd ?? raw.workingDirectory ?? raw.working_directory ?? process.cwd(),
|
|
95
|
+
matchesPattern,
|
|
96
|
+
normalizePath,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writes a normalized hook response to stdout and sets the exit code.
|
|
102
|
+
* @param {{ decision?: 'allow' | 'deny', reason?: string, additionalContext?: string, continue?: boolean }} result
|
|
103
|
+
*/
|
|
104
|
+
export function respond(result = { decision: 'allow' }) {
|
|
105
|
+
const decision = result.decision ?? 'allow';
|
|
106
|
+
const payload =
|
|
107
|
+
decision === 'deny'
|
|
108
|
+
? { decision: 'deny', reason: result.reason ?? 'Hook denied request.' }
|
|
109
|
+
: {
|
|
110
|
+
...(result.additionalContext ? { additionalContext: result.additionalContext } : {}),
|
|
111
|
+
...(result.continue === false ? { continue: false } : {}),
|
|
112
|
+
};
|
|
113
|
+
if (Object.keys(payload).length > 0) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
114
|
+
process.exitCode = decision === 'deny' ? 2 : 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extracts file paths from common tool input field names.
|
|
119
|
+
* @param {unknown} toolInput
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
export function extractFilePaths(toolInput) {
|
|
123
|
+
const found = new Set();
|
|
124
|
+
const visit = (value, key = '') => {
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
value.forEach((item) => {
|
|
127
|
+
visit(item, key);
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (value && typeof value === 'object') {
|
|
132
|
+
Object.entries(value).forEach(([nextKey, nextValue]) => {
|
|
133
|
+
visit(nextValue, nextKey);
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === 'string' && FILE_KEYS.has(key)) found.add(normalizePath(value));
|
|
138
|
+
};
|
|
139
|
+
visit(toolInput);
|
|
140
|
+
return [...found].filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Matches a normalized path against simple wildcard and directory-prefix patterns.
|
|
145
|
+
* @param {string} filePath
|
|
146
|
+
* @param {string[]} patterns
|
|
147
|
+
*/
|
|
148
|
+
export function matchesPattern(filePath, patterns) {
|
|
149
|
+
const target = normalizePath(filePath);
|
|
150
|
+
return patterns.some((pattern) => {
|
|
151
|
+
const normalizedPattern = normalizePath(pattern);
|
|
152
|
+
if (!normalizedPattern) return false;
|
|
153
|
+
if (normalizedPattern.endsWith('/')) {
|
|
154
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
155
|
+
return target === prefix || target.includes(`/${prefix}/`) || target.startsWith(`${prefix}/`);
|
|
156
|
+
}
|
|
157
|
+
return new RegExp(`(^|/)${normalizedPattern.split('*').map(escapeRegex).join('.*')}$`).test(
|
|
158
|
+
target,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { createHook } from './_runtime.mjs';
|
|
7
|
+
|
|
8
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'create_file', 'editFiles', 'replace_string_in_file']);
|
|
9
|
+
const counterPath = path.join(os.tmpdir(), `aikit-edit-count-${process.ppid}.json`);
|
|
10
|
+
const readCount = () => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(counterPath, 'utf8')).count || 0;
|
|
13
|
+
} catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const writeCount = (count) => {
|
|
18
|
+
try {
|
|
19
|
+
fs.writeFileSync(counterPath, JSON.stringify({ count }), 'utf8');
|
|
20
|
+
} catch {}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Tracks edit counts and nudges validation after repeated file changes. */
|
|
24
|
+
export const postEditCheck = async (context) => {
|
|
25
|
+
if (context.event !== 'PostToolUse' || !EDIT_TOOLS.has(context.toolName))
|
|
26
|
+
return { decision: 'allow' };
|
|
27
|
+
const count = readCount() + 1;
|
|
28
|
+
writeCount(count);
|
|
29
|
+
if (count < 5 || count % 5 !== 0) return { decision: 'allow' };
|
|
30
|
+
const prefix = count >= 10 ? 'Strongly consider' : 'Consider';
|
|
31
|
+
return {
|
|
32
|
+
additionalContext: `📋 You've made ${count} file edits this session. ${prefix} running check({}) and test_run({}) to validate changes before continuing.`,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
createHook(postEditCheck);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHook } from './_runtime.mjs';
|
|
3
|
+
|
|
4
|
+
const REMINDER = `⚠️ Context compaction imminent. Before proceeding:
|
|
5
|
+
1. If you have uncommitted decisions, call: knowledge({ action: "remember", title: "Pre-compact save", content: "<key decisions and state>", category: "session" })
|
|
6
|
+
2. If a flow is active, call: stash({ action: "set", key: "pre-compact-state", value: "<current progress>" })
|
|
7
|
+
3. After compaction, call: search({ query: "SESSION CHECKPOINT", origin: "curated" }) to recover context.`;
|
|
8
|
+
|
|
9
|
+
/** Injects a save-state reminder immediately before context compaction. */
|
|
10
|
+
export const preCompactSave = async (context) =>
|
|
11
|
+
context.event === 'PreCompact' ? { additionalContext: REMINDER } : { decision: 'allow' };
|
|
12
|
+
|
|
13
|
+
createHook(preCompactSave);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHook } from './_runtime.mjs';
|
|
3
|
+
|
|
4
|
+
const BLOCKED = [
|
|
5
|
+
'**/.env',
|
|
6
|
+
'**/.env.*',
|
|
7
|
+
'**/*.pem',
|
|
8
|
+
'**/*.key',
|
|
9
|
+
'**/*.p12',
|
|
10
|
+
'**/*.pfx',
|
|
11
|
+
'**/id_rsa*',
|
|
12
|
+
'**/id_ed25519*',
|
|
13
|
+
'**/.ssh/*',
|
|
14
|
+
'**/*credentials*',
|
|
15
|
+
'**/*.secret',
|
|
16
|
+
'**/.netrc',
|
|
17
|
+
'**/.pgpass',
|
|
18
|
+
'**/secret.*',
|
|
19
|
+
'**/secrets.*',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const READ_TOOLS = new Set(['Read', 'read_file', 'readFile']);
|
|
23
|
+
|
|
24
|
+
/** Blocks reads of secret-bearing files and key material. */
|
|
25
|
+
export const privacyGuard = async (context) => {
|
|
26
|
+
if (context.event !== 'PreToolUse' || !READ_TOOLS.has(context.toolName))
|
|
27
|
+
return { decision: 'allow' };
|
|
28
|
+
const blockedPath = context.filePaths.find((filePath) =>
|
|
29
|
+
context.matchesPattern(filePath, BLOCKED),
|
|
30
|
+
);
|
|
31
|
+
return blockedPath
|
|
32
|
+
? {
|
|
33
|
+
decision: 'deny',
|
|
34
|
+
reason: `Blocked: reading sensitive file ${blockedPath}. Use environment variables or secrets manager instead.`,
|
|
35
|
+
}
|
|
36
|
+
: { decision: 'allow' };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
createHook(privacyGuard);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHook } from './_runtime.mjs';
|
|
3
|
+
|
|
4
|
+
const BLOCKED_DIRS = [
|
|
5
|
+
'node_modules/',
|
|
6
|
+
'.git/objects/',
|
|
7
|
+
'dist/',
|
|
8
|
+
'build/',
|
|
9
|
+
'vendor/',
|
|
10
|
+
'.next/',
|
|
11
|
+
'coverage/',
|
|
12
|
+
'__pycache__/',
|
|
13
|
+
'.tox/',
|
|
14
|
+
'target/debug/',
|
|
15
|
+
'target/release/',
|
|
16
|
+
'.gradle/',
|
|
17
|
+
'Pods/',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const normalize = (value) =>
|
|
21
|
+
String(value ?? '')
|
|
22
|
+
.replace(/\\/g, '/')
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
|
|
25
|
+
/** Blocks reads and searches inside generated or dependency-heavy directories. */
|
|
26
|
+
export const scoutGuard = async (context) => {
|
|
27
|
+
if (context.event !== 'PreToolUse') return { decision: 'allow' };
|
|
28
|
+
const blockedDir = context.filePaths.reduce((match, filePath) => {
|
|
29
|
+
if (match) return match;
|
|
30
|
+
const normalizedPath = normalize(filePath);
|
|
31
|
+
return (
|
|
32
|
+
BLOCKED_DIRS.find(
|
|
33
|
+
(dir) => normalizedPath.startsWith(dir) || normalizedPath.includes(`/${dir}`),
|
|
34
|
+
) ?? ''
|
|
35
|
+
);
|
|
36
|
+
}, '');
|
|
37
|
+
return blockedDir
|
|
38
|
+
? {
|
|
39
|
+
decision: 'deny',
|
|
40
|
+
reason: `Blocked: accessing ${blockedDir} wastes context. Use search tools or package documentation instead.`,
|
|
41
|
+
}
|
|
42
|
+
: { decision: 'allow' };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
createHook(scoutGuard);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { createHook } from './_runtime.mjs';
|
|
6
|
+
|
|
7
|
+
const exists = (cwd, name) => {
|
|
8
|
+
try {
|
|
9
|
+
return fs.existsSync(path.join(cwd, name));
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const list = (cwd) => {
|
|
15
|
+
try {
|
|
16
|
+
return fs.readdirSync(cwd);
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const readJson = (cwd, name) => {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, name), 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const detectPackageManager = (cwd, pkg) =>
|
|
29
|
+
pkg?.packageManager?.split('@')[0] ||
|
|
30
|
+
(exists(cwd, 'pnpm-lock.yaml') && 'pnpm') ||
|
|
31
|
+
(exists(cwd, 'yarn.lock') && 'yarn') ||
|
|
32
|
+
((exists(cwd, 'bun.lockb') || exists(cwd, 'bun.lock')) && 'bun') ||
|
|
33
|
+
(exists(cwd, 'package-lock.json') && 'npm') ||
|
|
34
|
+
'unknown';
|
|
35
|
+
|
|
36
|
+
/** Detects workspace metadata and injects stack context at session start. */
|
|
37
|
+
export const sessionInit = async (context) => {
|
|
38
|
+
if (context.event !== 'SessionStart') return { decision: 'allow' };
|
|
39
|
+
const pkg = readJson(context.cwd, 'package.json');
|
|
40
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
41
|
+
const entries = list(context.cwd);
|
|
42
|
+
const framework = deps.react
|
|
43
|
+
? 'React'
|
|
44
|
+
: deps.vue
|
|
45
|
+
? 'Vue'
|
|
46
|
+
: deps.angular
|
|
47
|
+
? 'Angular'
|
|
48
|
+
: deps.next
|
|
49
|
+
? 'Next.js'
|
|
50
|
+
: deps.express
|
|
51
|
+
? 'Express'
|
|
52
|
+
: deps.fastify
|
|
53
|
+
? 'Fastify'
|
|
54
|
+
: '';
|
|
55
|
+
const stack = pkg
|
|
56
|
+
? `Node.js${framework ? `, ${framework}` : ''}`
|
|
57
|
+
: exists(context.cwd, 'go.mod')
|
|
58
|
+
? 'Go'
|
|
59
|
+
: exists(context.cwd, 'Cargo.toml')
|
|
60
|
+
? 'Rust'
|
|
61
|
+
: exists(context.cwd, 'requirements.txt') || exists(context.cwd, 'pyproject.toml')
|
|
62
|
+
? 'Python'
|
|
63
|
+
: entries.some((name) => /\.(sln|csproj)$/i.test(name))
|
|
64
|
+
? '.NET'
|
|
65
|
+
: exists(context.cwd, 'pom.xml') || exists(context.cwd, 'build.gradle')
|
|
66
|
+
? 'Java/Kotlin'
|
|
67
|
+
: 'Unknown';
|
|
68
|
+
const monorepo = exists(context.cwd, 'pnpm-workspace.yaml')
|
|
69
|
+
? 'yes (pnpm-workspace)'
|
|
70
|
+
: exists(context.cwd, 'lerna.json')
|
|
71
|
+
? 'yes (lerna)'
|
|
72
|
+
: exists(context.cwd, 'packages')
|
|
73
|
+
? 'yes (packages/)'
|
|
74
|
+
: 'no';
|
|
75
|
+
const metadata = [
|
|
76
|
+
`Workspace: ${path.basename(context.cwd)}`,
|
|
77
|
+
`Stack: ${stack}`,
|
|
78
|
+
`Monorepo: ${monorepo}`,
|
|
79
|
+
`Package manager: ${detectPackageManager(context.cwd, pkg)}`,
|
|
80
|
+
...(pkg ? [`Node: ${process.version}`] : []),
|
|
81
|
+
].join('\n');
|
|
82
|
+
return { additionalContext: metadata };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
createHook(sessionInit);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session-learn — Stop event hook for autonomous learning.
|
|
4
|
+
*
|
|
5
|
+
* Fires at session end. Nudges agent to process buffered observations
|
|
6
|
+
* before context is lost. Cleans up buffer if too small for analysis.
|
|
7
|
+
*
|
|
8
|
+
* @tier efficiency
|
|
9
|
+
* @event Stop
|
|
10
|
+
* @scope user
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
import { createHook } from './_runtime.mjs';
|
|
17
|
+
|
|
18
|
+
const bufferPath = path.join(os.tmpdir(), `aikit-obs-${process.ppid}.jsonl`);
|
|
19
|
+
const MIN_OBSERVATIONS = 10;
|
|
20
|
+
|
|
21
|
+
const getLineCount = (filePath) => {
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean).length;
|
|
24
|
+
} catch {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const cleanup = () => {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(bufferPath);
|
|
32
|
+
} catch {}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Nudges end-of-session lesson extraction and cleans up small buffers. */
|
|
36
|
+
export const sessionLearn = async (context) => {
|
|
37
|
+
if (context.event !== 'Stop') return { decision: 'allow' };
|
|
38
|
+
|
|
39
|
+
const count = getLineCount(bufferPath);
|
|
40
|
+
if (count < MIN_OBSERVATIONS) {
|
|
41
|
+
cleanup();
|
|
42
|
+
return { decision: 'allow' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
additionalContext: [
|
|
47
|
+
`[Observer] Session ending with ${count} observations buffered.`,
|
|
48
|
+
'Run knowledge({ action: "lesson", subAction: "auto-observe" }) to extract learning patterns.',
|
|
49
|
+
].join(' '),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
createHook(sessionLearn);
|