@su-record/vibe 4.2.35 → 4.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli/common.js +4 -1
- package/dist/cli/common.js.map +1 -1
- package/dist/cli/heldout.js +10 -4
- package/dist/cli/heldout.js.map +1 -1
- package/dist/cli/human.js +8 -4
- package/dist/cli/human.js.map +1 -1
- package/dist/cli/internal.js +37 -24
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/ledger.js +67 -0
- package/dist/cli/ledger.js.map +1 -0
- package/dist/cli/map.js +5 -3
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/memory.js +104 -74
- package/dist/cli/memory.js.map +1 -1
- package/dist/cli/mutate.js +20 -0
- package/dist/cli/mutate.js.map +1 -0
- package/dist/cli/risks.js +20 -3
- package/dist/cli/risks.js.map +1 -1
- package/dist/cli/session.js +4 -2
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/setup.js +44 -13
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/work.js +73 -21
- package/dist/cli/work.js.map +1 -1
- package/dist/cli.js +19 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/analyze.js +21 -7
- package/dist/core/analyze.js.map +1 -1
- package/dist/core/artifact-paths.js +14 -3
- package/dist/core/artifact-paths.js.map +1 -1
- package/dist/core/capabilities.js +69 -15
- package/dist/core/capabilities.js.map +1 -1
- package/dist/core/check-authorization.js +37 -0
- package/dist/core/check-authorization.js.map +1 -0
- package/dist/core/check-process.js +12 -4
- package/dist/core/check-process.js.map +1 -1
- package/dist/core/check-report.js +66 -0
- package/dist/core/check-report.js.map +1 -0
- package/dist/core/check.js +59 -71
- package/dist/core/check.js.map +1 -1
- package/dist/core/checks/changed.js +20 -6
- package/dist/core/checks/changed.js.map +1 -1
- package/dist/core/checks/eval.js +35 -10
- package/dist/core/checks/eval.js.map +1 -1
- package/dist/core/checks/file.js +26 -23
- package/dist/core/checks/file.js.map +1 -1
- package/dist/core/checks/fileRules.js +42 -16
- package/dist/core/checks/fileRules.js.map +1 -1
- package/dist/core/checks/http.js +35 -34
- package/dist/core/checks/http.js.map +1 -1
- package/dist/core/checks/packs.js +1 -1
- package/dist/core/checks/packs.js.map +1 -1
- package/dist/core/checks/review.js +75 -40
- package/dist/core/checks/review.js.map +1 -1
- package/dist/core/checks/run.js +5 -2
- package/dist/core/checks/run.js.map +1 -1
- package/dist/core/checks/schema.js +25 -0
- package/dist/core/checks/schema.js.map +1 -0
- package/dist/core/checks/source.js +11 -6
- package/dist/core/checks/source.js.map +1 -1
- package/dist/core/config.js +9 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/consent.js +24 -16
- package/dist/core/consent.js.map +1 -1
- package/dist/core/context-notes.js +15 -7
- package/dist/core/context-notes.js.map +1 -1
- package/dist/core/context.js +24 -12
- package/dist/core/context.js.map +1 -1
- package/dist/core/conventions.js +22 -9
- package/dist/core/conventions.js.map +1 -1
- package/dist/core/docs/cfb.js +81 -45
- package/dist/core/docs/cfb.js.map +1 -1
- package/dist/core/docs/html.js +14 -3
- package/dist/core/docs/html.js.map +1 -1
- package/dist/core/docs/hwp.js +44 -22
- package/dist/core/docs/hwp.js.map +1 -1
- package/dist/core/docs/office.js +20 -10
- package/dist/core/docs/office.js.map +1 -1
- package/dist/core/docs/pdf.js +10 -4
- package/dist/core/docs/pdf.js.map +1 -1
- package/dist/core/docs/read.js +32 -24
- package/dist/core/docs/read.js.map +1 -1
- package/dist/core/docs/zip.js +32 -15
- package/dist/core/docs/zip.js.map +1 -1
- package/dist/core/evidence.js +17 -7
- package/dist/core/evidence.js.map +1 -1
- package/dist/core/failure-message.js +35 -13
- package/dist/core/failure-message.js.map +1 -1
- package/dist/core/failure.js +49 -14
- package/dist/core/failure.js.map +1 -1
- package/dist/core/github.js +3 -1
- package/dist/core/github.js.map +1 -1
- package/dist/core/handoff.js +20 -7
- package/dist/core/handoff.js.map +1 -1
- package/dist/core/heldout-run.js +6 -3
- package/dist/core/heldout-run.js.map +1 -1
- package/dist/core/heldout.js +24 -13
- package/dist/core/heldout.js.map +1 -1
- package/dist/core/inbox.js +3 -1
- package/dist/core/inbox.js.map +1 -1
- package/dist/core/inputs.js +6 -3
- package/dist/core/inputs.js.map +1 -1
- package/dist/core/inspect.js +61 -20
- package/dist/core/inspect.js.map +1 -1
- package/dist/core/intent.js +35 -13
- package/dist/core/intent.js.map +1 -1
- package/dist/core/knowledge-find.js +31 -12
- package/dist/core/knowledge-find.js.map +1 -1
- package/dist/core/knowledge.js +22 -12
- package/dist/core/knowledge.js.map +1 -1
- package/dist/core/lang.js +5 -2
- package/dist/core/lang.js.map +1 -1
- package/dist/core/ledger.js +44 -20
- package/dist/core/ledger.js.map +1 -1
- package/dist/core/locations.js +12 -0
- package/dist/core/locations.js.map +1 -0
- package/dist/core/map/imports.js +20 -9
- package/dist/core/map/imports.js.map +1 -1
- package/dist/core/map/index.js +28 -13
- package/dist/core/map/index.js.map +1 -1
- package/dist/core/map/symbols.js +27 -12
- package/dist/core/map/symbols.js.map +1 -1
- package/dist/core/mutate.js +107 -0
- package/dist/core/mutate.js.map +1 -0
- package/dist/core/output-capture.js +7 -4
- package/dist/core/output-capture.js.map +1 -1
- package/dist/core/paths.js +2 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/performance.js +43 -17
- package/dist/core/performance.js.map +1 -1
- package/dist/core/profile.js +6 -3
- package/dist/core/profile.js.map +1 -1
- package/dist/core/reader.js +32 -16
- package/dist/core/reader.js.map +1 -1
- package/dist/core/readerSession.js +34 -14
- package/dist/core/readerSession.js.map +1 -1
- package/dist/core/recent.js +20 -6
- package/dist/core/recent.js.map +1 -1
- package/dist/core/regress.js +21 -12
- package/dist/core/regress.js.map +1 -1
- package/dist/core/repair.js +22 -4
- package/dist/core/repair.js.map +1 -1
- package/dist/core/research-queries.js +85 -0
- package/dist/core/research-queries.js.map +1 -0
- package/dist/core/research.js +89 -88
- package/dist/core/research.js.map +1 -1
- package/dist/core/results.js +15 -6
- package/dist/core/results.js.map +1 -1
- package/dist/core/risk-signals.js +53 -15
- package/dist/core/risk-signals.js.map +1 -1
- package/dist/core/run-id.js +10 -6
- package/dist/core/run-id.js.map +1 -1
- package/dist/core/scenarios.js +91 -60
- package/dist/core/scenarios.js.map +1 -1
- package/dist/core/session.js +3 -2
- package/dist/core/session.js.map +1 -1
- package/dist/core/size.js +28 -8
- package/dist/core/size.js.map +1 -1
- package/dist/core/skills.js +66 -28
- package/dist/core/skills.js.map +1 -1
- package/dist/core/skillsmp.js +26 -12
- package/dist/core/skillsmp.js.map +1 -1
- package/dist/core/source-basis.js +21 -9
- package/dist/core/source-basis.js.map +1 -1
- package/dist/core/status.js +26 -13
- package/dist/core/status.js.map +1 -1
- package/dist/core/table.js +8 -1
- package/dist/core/table.js.map +1 -1
- package/dist/core/tokens.js +20 -10
- package/dist/core/tokens.js.map +1 -1
- package/dist/core/tree.js +26 -16
- package/dist/core/tree.js.map +1 -1
- package/dist/core/verification-plan.js +24 -8
- package/dist/core/verification-plan.js.map +1 -1
- package/dist/core/verification.js +14 -7
- package/dist/core/verification.js.map +1 -1
- package/dist/core/view.js +93 -44
- package/dist/core/view.js.map +1 -1
- package/dist/install/global.js +37 -18
- package/dist/install/global.js.map +1 -1
- package/dist/install/hooks.js +17 -8
- package/dist/install/hooks.js.map +1 -1
- package/dist/install/manifest.js +11 -0
- package/dist/install/manifest.js.map +1 -0
- package/dist/install/mcpb.js +52 -23
- package/dist/install/mcpb.js.map +1 -1
- package/dist/install/plugin.js +73 -33
- package/dist/install/plugin.js.map +1 -1
- package/dist/install/project.js +8 -7
- package/dist/install/project.js.map +1 -1
- package/dist/install/register.js +83 -48
- package/dist/install/register.js.map +1 -1
- package/dist/install/tree.js +27 -11
- package/dist/install/tree.js.map +1 -1
- package/dist/install/update.js +46 -20
- package/dist/install/update.js.map +1 -1
- package/hooks/code-map.cjs +22 -9
- package/hooks/journal.cjs +18 -16
- package/hooks/knowledge-auto.cjs +55 -21
- package/hooks/knowledge-journal.cjs +47 -20
- package/hooks/knowledge-projects.cjs +50 -21
- package/hooks/knowledge-recall.cjs +16 -6
- package/hooks/knowledge-upkeep.cjs +22 -5
- package/hooks/locations.cjs +35 -0
- package/hooks/notify.js +52 -24
- package/hooks/obsidian.cjs +17 -7
- package/hooks/private-store.cjs +25 -10
- package/hooks/retest.cjs +39 -16
- package/hooks/session-context.cjs +30 -13
- package/hooks/session-files.cjs +44 -21
- package/hooks/session-repair.cjs +26 -11
- package/hooks/session-state.cjs +78 -38
- package/hooks/session.js +19 -8
- package/hooks/slice-command.cjs +38 -17
- package/hooks/slice-guard.cjs +20 -6
- package/hooks/slice-store.cjs +25 -12
- package/internal/guides/code.md +2 -0
- package/mcpb/server/index.js +40 -12
- package/package.json +1 -1
package/hooks/session.js
CHANGED
|
@@ -5,13 +5,25 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import codeMap from './code-map.cjs';
|
|
7
7
|
import knowledgeAuto from './knowledge-auto.cjs';
|
|
8
|
+
import locations from './locations.cjs';
|
|
8
9
|
import sessionRuntime from './session-state.cjs';
|
|
9
10
|
import sessionFiles from './session-files.cjs';
|
|
10
11
|
|
|
12
|
+
const { claudeHome, codexHome } = locations;
|
|
13
|
+
|
|
14
|
+
const READ_MAX_BYTES = 64 * 1024;
|
|
15
|
+
const CARD_MAX_BYTES = 4096;
|
|
16
|
+
const CARD_FILE = 'card.md';
|
|
17
|
+
// The marker vibe's installer wraps its section of the home instruction file in.
|
|
18
|
+
const HOME_MARKER = '<!-- vibe:start -->';
|
|
19
|
+
|
|
11
20
|
const client = process.argv[2] || 'claude';
|
|
12
21
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
22
|
const home = process.env.VIBE_HOME_DIR || os.homedir();
|
|
14
|
-
|
|
23
|
+
const emit = (hookEventName, additionalContext, extra = {}) => {
|
|
24
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext }, ...extra })}\n`);
|
|
25
|
+
};
|
|
26
|
+
function read(file, limit = READ_MAX_BYTES) {
|
|
15
27
|
try { return sessionFiles.optional(file, limit) ?? ''; }
|
|
16
28
|
catch { return ''; }
|
|
17
29
|
}
|
|
@@ -19,21 +31,20 @@ function payload() {
|
|
|
19
31
|
try { return sessionFiles.readPayload(); }
|
|
20
32
|
catch { return { session_id: 'invalid payload' }; }
|
|
21
33
|
}
|
|
22
|
-
const card = read(path.join(root,
|
|
23
|
-
const homeCard = client === 'claude' ?
|
|
24
|
-
const homeOwns = read(homeCard).includes(
|
|
34
|
+
const card = read(path.join(root, CARD_FILE), CARD_MAX_BYTES).trim();
|
|
35
|
+
const homeCard = client === 'claude' ? claudeHome(home, 'CLAUDE.md') : codexHome(home, 'AGENTS.md');
|
|
36
|
+
const homeOwns = read(homeCard).includes(HOME_MARKER);
|
|
25
37
|
// SubagentStart: session context never reaches a subagent, so each one gets the card the home card does not already carry.
|
|
26
38
|
if (process.argv[3] === 'subagent') {
|
|
27
|
-
if (!homeOwns && card)
|
|
39
|
+
if (!homeOwns && card) emit('SubagentStart', card);
|
|
28
40
|
process.exit(0);
|
|
29
41
|
}
|
|
30
42
|
const input = payload();
|
|
31
43
|
const recall = knowledgeAuto.start(input);
|
|
32
44
|
const view = sessionRuntime.sessionStatus(input);
|
|
33
|
-
const
|
|
34
|
-
const note = sessionRuntime.message(view, status);
|
|
45
|
+
const note = sessionRuntime.structuralNote(view);
|
|
35
46
|
const map = codeMap.codeMap(process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd());
|
|
36
47
|
const text = [homeOwns ? note : `${card}\n\n${note}`, recall, map].filter(Boolean).join('\n');
|
|
37
48
|
// Codex has no status surface a plugin can draw on; a systemMessage is the one line it shows the person.
|
|
38
49
|
const line = client === 'codex' && view.root ? sessionRuntime.overview(view.root) : null;
|
|
39
|
-
|
|
50
|
+
emit('SessionStart', text, line ? { systemMessage: line } : {});
|
package/hooks/slice-command.cjs
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
|
|
3
|
+
const MAX_COMMAND_CHARS = 64 * 1024;
|
|
4
|
+
const MAX_TOKENS = 2048;
|
|
5
|
+
const MAX_TARGETS = 256;
|
|
6
|
+
// A nested `bash -c` is followed once.
|
|
7
|
+
const MAX_NESTING = 1;
|
|
8
|
+
|
|
3
9
|
// A bounded lexical recognizer, never a shell evaluator. Dynamic expansions remain unresolved.
|
|
4
10
|
function tokenize(text) {
|
|
5
|
-
if (typeof text !== 'string' || text.length >
|
|
11
|
+
if (typeof text !== 'string' || text.length > MAX_COMMAND_CHARS) return null;
|
|
6
12
|
const tokens = [];
|
|
7
13
|
let word = '', quote = '';
|
|
8
14
|
const flush = () => { if (word) tokens.push(word); word = ''; };
|
|
@@ -17,7 +23,7 @@ function tokenize(text) {
|
|
|
17
23
|
else if (/[\n|;&<>]/.test(ch)) { flush(); tokens.push(['|', '&', '<', '>'].includes(ch) && text[i + 1] === ch ? ch + text[++i] : ch); }
|
|
18
24
|
else if (/\s/.test(ch)) flush();
|
|
19
25
|
else word += ch;
|
|
20
|
-
if (tokens.length >
|
|
26
|
+
if (tokens.length > MAX_TOKENS) return null;
|
|
21
27
|
}
|
|
22
28
|
if (quote) return null;
|
|
23
29
|
flush();
|
|
@@ -60,43 +66,58 @@ function operands(args, valued = []) {
|
|
|
60
66
|
}
|
|
61
67
|
return out;
|
|
62
68
|
}
|
|
69
|
+
const NAMES_ONLY_OPTION = /^--files(?:-with-matches|-without-match)?$|^-(?!-)[A-Za-z]*[lL][A-Za-z]*$/;
|
|
70
|
+
// grep and rg options whose next word is a value, not a file.
|
|
71
|
+
const GREP_VALUED = [
|
|
72
|
+
'-e', '--regexp', '-f', '--file', '-g', '--glob', '-t', '--type', '-T', '--type-not', '-m', '--max-count', '--max-columns', '--max-depth',
|
|
73
|
+
'-A', '-B', '-C',
|
|
74
|
+
];
|
|
75
|
+
// Options of the line transformers whose next word is a value, not a file.
|
|
76
|
+
const TRANSFORM_VALUED = {
|
|
77
|
+
nl: [
|
|
78
|
+
'-b', '-d', '-f', '-h', '-i', '-l', '-n', '-s', '-v', '--body-numbering', '--section-delimiter', '--footer-numbering',
|
|
79
|
+
'--header-numbering', '--line-increment', '--join-blank-lines', '--number-format', '--number-separator', '--starting-line-number',
|
|
80
|
+
],
|
|
81
|
+
sort: ['-k', '-t', '-o', '-T', '-S', '--key', '--field-separator', '--output', '--temporary-directory', '--buffer-size', '--parallel'],
|
|
82
|
+
uniq: ['-f', '-s', '-w', '--skip-fields', '--skip-chars', '--check-chars'],
|
|
83
|
+
};
|
|
84
|
+
const CUT_VALUED = ['-b', '-c', '-f', '-d', '--bytes', '--characters', '--fields', '--delimiter', '--output-delimiter'];
|
|
85
|
+
const HEAD_VALUED = ['-n', '-c', '--lines', '--bytes', '--sleep-interval'];
|
|
86
|
+
const SCRIPT_VALUED = ['-e', '-f', '--expression', '--file', '-F', '-v'];
|
|
63
87
|
function grepInfo(name, args) {
|
|
64
88
|
const options = args.filter(word => word.startsWith('-'));
|
|
65
|
-
const namesOnly = options.some(word =>
|
|
89
|
+
const namesOnly = options.some(word => NAMES_ONLY_OPTION.test(word));
|
|
66
90
|
const locations = namesOnly || options.some(word => word === '--line-number' || /^-[^-]*n/.test(word));
|
|
67
91
|
const explicitPattern = args.some(word => ['-e', '--regexp', '-f', '--file'].includes(word) || /^--(?:regexp|file)=/.test(word));
|
|
68
|
-
const files = operands(args,
|
|
92
|
+
const files = operands(args, GREP_VALUED);
|
|
69
93
|
if (!explicitPattern && !options.includes('--files')) files.shift();
|
|
70
94
|
return { files, slice: !locations, namesOnly, implicit: name === 'rg' || options.some(word => /^-[^-]*[rR]/.test(word)) };
|
|
71
95
|
}
|
|
72
96
|
function transformInfo(name, args) {
|
|
73
97
|
if (name === 'tr') return { files: args.flatMap((word, index) => word === '<' && args[index + 1] ? [args[index + 1]] : []), slice: false };
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
sort: ['-k', '-t', '-o', '-T', '-S', '--key', '--field-separator', '--output', '--temporary-directory', '--buffer-size', '--parallel'],
|
|
77
|
-
uniq: ['-f', '-s', '-w', '--skip-fields', '--skip-chars', '--check-chars'],
|
|
78
|
-
};
|
|
79
|
-
if (!Object.hasOwn(values, name)) return null;
|
|
80
|
-
const files = operands(args, values[name]);
|
|
98
|
+
if (!Object.hasOwn(TRANSFORM_VALUED, name)) return null;
|
|
99
|
+
const files = operands(args, TRANSFORM_VALUED[name]);
|
|
81
100
|
return { files: name === 'uniq' ? files.slice(0, 1) : files, slice: false };
|
|
82
101
|
}
|
|
83
102
|
function readerInfo(name, args) {
|
|
84
103
|
if (['grep', 'rg'].includes(name)) return grepInfo(name, args);
|
|
85
104
|
if (name === 'cat') return { files: operands(args), slice: false };
|
|
86
|
-
if (['head', 'tail'].includes(name)) return { files: operands(args,
|
|
87
|
-
if (name === 'cut') return { files: operands(args,
|
|
105
|
+
if (['head', 'tail'].includes(name)) return { files: operands(args, HEAD_VALUED), slice: true };
|
|
106
|
+
if (name === 'cut') return { files: operands(args, CUT_VALUED), slice: true };
|
|
88
107
|
if (['sed', 'awk'].includes(name)) {
|
|
89
108
|
const explicit = args.some(word => ['-e', '-f', '--expression', '--file'].includes(word));
|
|
90
|
-
const files = operands(args,
|
|
109
|
+
const files = operands(args, SCRIPT_VALUED);
|
|
91
110
|
if (!explicit) files.shift();
|
|
92
|
-
const
|
|
111
|
+
const awkSlice = word => /\bNR\b|\bprint\b/.test(word);
|
|
112
|
+
const sedSlice = word => /^-[^-]*n/.test(word) || word === '--quiet' || word === '--silent';
|
|
113
|
+
const slice = args.some(name === 'awk' ? awkSlice : sedSlice);
|
|
93
114
|
return { files, slice };
|
|
94
115
|
}
|
|
95
116
|
return transformInfo(name, args);
|
|
96
117
|
}
|
|
97
118
|
function sliceTargets(text, cwd, depth = 0) {
|
|
98
119
|
const tokens = tokenize(text);
|
|
99
|
-
if (!tokens || tokens.includes('<<') || depth >
|
|
120
|
+
if (!tokens || tokens.includes('<<') || depth > MAX_NESTING) return { unavailable: true, files: [] };
|
|
100
121
|
const found = new Set();
|
|
101
122
|
let input = [], directory = cwd;
|
|
102
123
|
for (const section of sections(tokens)) {
|
|
@@ -116,6 +137,6 @@ function sliceTargets(text, cwd, depth = 0) {
|
|
|
116
137
|
if (info.slice) for (const file of input) found.add(file);
|
|
117
138
|
if (info.namesOnly) input = [];
|
|
118
139
|
}
|
|
119
|
-
return { unavailable: false, files: [...found].slice(0,
|
|
140
|
+
return { unavailable: false, files: [...found].slice(0, MAX_TARGETS) };
|
|
120
141
|
}
|
|
121
142
|
module.exports = { sliceTargets };
|
package/hooks/slice-guard.cjs
CHANGED
|
@@ -2,6 +2,15 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { sliceTargets } = require('./slice-command.cjs');
|
|
4
4
|
const { readTargets, recordSlice } = require('./slice-store.cjs');
|
|
5
|
+
/** Past this many lines a file only to be understood is better read by `vibe read --ask`. */
|
|
6
|
+
const READ_ADVISE_LINES = 400;
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|c|cpp|h|cs|rb|sh|sql|md|mdx|txt|rst|json|ya?ml|toml|ini|xml|html|css|csv|tsv)$/i;
|
|
9
|
+
const SOURCE_NAME = /^(?:README|Dockerfile|Makefile)(?:\..*)?$/i;
|
|
10
|
+
const PROTECTED_PATH = /(?:^|\/)scenarios\.ya?ml$|(?:^|\/)specs?(?:\/|$)/i;
|
|
11
|
+
const BLOCKED = 'Partial reading of a protected file is blocked.';
|
|
12
|
+
const WARNED = 'Partial source/document reading can omit requirements.';
|
|
13
|
+
const INTENT_FILES = ['.vibe', '.vibe/intent.md'];
|
|
5
14
|
|
|
6
15
|
function within(directory, file) {
|
|
7
16
|
const relative = path.relative(directory, file);
|
|
@@ -13,17 +22,20 @@ function exempt(root, file) {
|
|
|
13
22
|
}
|
|
14
23
|
function protectedFile(root, file, files) {
|
|
15
24
|
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
16
|
-
if (
|
|
25
|
+
if (INTENT_FILES.includes(relative) || PROTECTED_PATH.test(relative)) return true;
|
|
17
26
|
return files.some(protectedPath => file === protectedPath || within(file, protectedPath));
|
|
18
27
|
}
|
|
19
28
|
function sourceFile(file) {
|
|
20
|
-
if (
|
|
21
|
-
if (/^(?:README|Dockerfile|Makefile)(?:\..*)?$/i.test(path.basename(file))) return true;
|
|
29
|
+
if (SOURCE_EXTENSION.test(file) || SOURCE_NAME.test(path.basename(file))) return true;
|
|
22
30
|
try { return fs.lstatSync(file).isDirectory(); } catch { return false; }
|
|
23
31
|
}
|
|
24
32
|
function guidance(client) {
|
|
25
33
|
const tool = client === 'codex' ? 'cat (the entire file)' : 'the Read tool (the entire file)';
|
|
26
|
-
return
|
|
34
|
+
return [
|
|
35
|
+
`Open the selected files in full with ${tool}.`,
|
|
36
|
+
'Read fewer files from vibe state; use grep/rg/find only to locate files or lines.',
|
|
37
|
+
`For over ${READ_ADVISE_LINES} lines when not editing, use vibe read --ask.`,
|
|
38
|
+
].join(' ');
|
|
27
39
|
}
|
|
28
40
|
function sliceGuard(root, command, client = 'claude', cwd = root) {
|
|
29
41
|
try { root = fs.realpathSync(root); cwd = fs.realpathSync(path.resolve(root, cwd)); } catch { return { decision: 'none', unavailable: true }; }
|
|
@@ -36,6 +48,8 @@ function sliceGuard(root, command, client = 'claude', cwd = root) {
|
|
|
36
48
|
if (decision === 'none') return { decision, unavailable: !mirror.available };
|
|
37
49
|
const recorded = recordSlice(root, decision);
|
|
38
50
|
const unavailable = !mirror.available || !recorded;
|
|
39
|
-
|
|
51
|
+
const headline = decision === 'block' ? BLOCKED : WARNED;
|
|
52
|
+
const storage = unavailable ? ' Read-target mirror or count storage unavailable.' : '';
|
|
53
|
+
return { decision, unavailable, recorded, message: `[vibe] ${headline} ${guidance(client)}${storage}` };
|
|
40
54
|
}
|
|
41
|
-
module.exports = { sliceGuard };
|
|
55
|
+
module.exports = { READ_ADVISE_LINES, sliceGuard };
|
package/hooks/slice-store.cjs
CHANGED
|
@@ -2,8 +2,17 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { randomBytes } = require('node:crypto');
|
|
4
4
|
const { privateDirectory, readPrivate, writePrivate } = require('./private-store.cjs');
|
|
5
|
-
const { digest, json } = require('./session-files.cjs');
|
|
5
|
+
const { STATE_FILE, INTENT_HASH, digest, json } = require('./session-files.cjs');
|
|
6
6
|
|
|
7
|
+
const MAX_TARGETS = 2048;
|
|
8
|
+
const MAX_PATH_CHARS = 4096;
|
|
9
|
+
const TARGETS_MAX_BYTES = 128 * 1024;
|
|
10
|
+
const MARKER_MAX_BYTES = 4096;
|
|
11
|
+
// One tiny file per recorded slice; a directory with more than this many store files is not read.
|
|
12
|
+
const MAX_STORE_FILES = 32768;
|
|
13
|
+
const COUNT_MAX_BYTES = 128;
|
|
14
|
+
const COUNT_SUFFIX_BYTES = 12;
|
|
15
|
+
const targetsName = root => `read-targets-${digest(root)}.json`;
|
|
7
16
|
const prefix = root => `slice-${digest(root)}-`;
|
|
8
17
|
const markerName = root => `slice-counts-${digest(root)}.json`;
|
|
9
18
|
function explicitHome(env) {
|
|
@@ -15,20 +24,24 @@ function explicitHome(env) {
|
|
|
15
24
|
function recordReadTargets(root, input, env) {
|
|
16
25
|
try {
|
|
17
26
|
root = fs.realpathSync(root);
|
|
18
|
-
if (!input ||
|
|
27
|
+
if (!input || !INTENT_HASH.test(input.intentHash) || !Array.isArray(input.files) || input.files.length > MAX_TARGETS) return false;
|
|
28
|
+
if (input.files.some(file => typeof file !== 'string' || file.length >= MAX_PATH_CHARS)) return false;
|
|
19
29
|
const files = [...new Set(input.files.map(file => path.resolve(root, file)))];
|
|
20
30
|
const body = JSON.stringify({ schemaVersion: 1, root, intentHash: input.intentHash, files });
|
|
21
|
-
if (Buffer.byteLength(body) >
|
|
22
|
-
writePrivate(root,
|
|
31
|
+
if (Buffer.byteLength(body) > TARGETS_MAX_BYTES) return false;
|
|
32
|
+
writePrivate(root, targetsName(root), body, explicitHome(env));
|
|
23
33
|
return true;
|
|
24
34
|
} catch { return false; }
|
|
25
35
|
}
|
|
26
36
|
function readTargets(root) {
|
|
27
37
|
try {
|
|
28
|
-
const raw = readPrivate(root,
|
|
38
|
+
const raw = readPrivate(root, targetsName(root), TARGETS_MAX_BYTES);
|
|
29
39
|
if (raw === null) return { available: false, files: [] };
|
|
30
|
-
const saved = JSON.parse(raw), state = json(path.join(root,
|
|
31
|
-
|
|
40
|
+
const saved = JSON.parse(raw), state = json(path.join(root, STATE_FILE));
|
|
41
|
+
const valid = saved.schemaVersion === 1 && saved.root === root && saved.intentHash === state.intentHash
|
|
42
|
+
&& Array.isArray(saved.files) && saved.files.length <= MAX_TARGETS
|
|
43
|
+
&& saved.files.every(file => typeof file === 'string' && path.isAbsolute(file));
|
|
44
|
+
if (!valid) return { available: false, files: [] };
|
|
32
45
|
return { available: true, files: saved.files };
|
|
33
46
|
} catch { return { available: false, files: [] }; }
|
|
34
47
|
}
|
|
@@ -37,7 +50,7 @@ function recordSlice(root, decision) {
|
|
|
37
50
|
if (!['block', 'warn'].includes(decision)) return false;
|
|
38
51
|
if (!initializeSliceCounts(root)) return false;
|
|
39
52
|
const counts = { blocked: Number(decision === 'block'), warned: Number(decision === 'warn') };
|
|
40
|
-
writePrivate(root, `${prefix(root)}${randomBytes(
|
|
53
|
+
writePrivate(root, `${prefix(root)}${randomBytes(COUNT_SUFFIX_BYTES).toString('hex')}.json`, JSON.stringify(counts));
|
|
41
54
|
return true;
|
|
42
55
|
} catch { return false; }
|
|
43
56
|
}
|
|
@@ -45,7 +58,7 @@ function initializeSliceCounts(root, env) {
|
|
|
45
58
|
try {
|
|
46
59
|
root = fs.realpathSync(root);
|
|
47
60
|
const home = explicitHome(env);
|
|
48
|
-
const raw = readPrivate(root, markerName(root),
|
|
61
|
+
const raw = readPrivate(root, markerName(root), MARKER_MAX_BYTES, home);
|
|
49
62
|
if (raw !== null) { const value = JSON.parse(raw); return value.schemaVersion === 1 && value.root === root; }
|
|
50
63
|
writePrivate(root, markerName(root), JSON.stringify({ schemaVersion: 1, root }), home);
|
|
51
64
|
return true;
|
|
@@ -55,14 +68,14 @@ function readSliceCounts(root, env) {
|
|
|
55
68
|
try {
|
|
56
69
|
root = fs.realpathSync(root);
|
|
57
70
|
const home = explicitHome(env);
|
|
58
|
-
const marker = JSON.parse(readPrivate(root, markerName(root),
|
|
71
|
+
const marker = JSON.parse(readPrivate(root, markerName(root), MARKER_MAX_BYTES, home));
|
|
59
72
|
if (!marker || marker.schemaVersion !== 1 || marker.root !== root) return null;
|
|
60
73
|
const directory = privateDirectory(root, false, home);
|
|
61
74
|
const files = fs.readdirSync(directory);
|
|
62
|
-
if (files.length >
|
|
75
|
+
if (files.length > MAX_STORE_FILES) return null;
|
|
63
76
|
const counts = { blocked: 0, warned: 0 };
|
|
64
77
|
for (const file of files.filter(file => file.startsWith(prefix(root)))) {
|
|
65
|
-
const value = JSON.parse(readPrivate(root, file,
|
|
78
|
+
const value = JSON.parse(readPrivate(root, file, COUNT_MAX_BYTES, home));
|
|
66
79
|
if (!((value.blocked === 1 && value.warned === 0) || (value.blocked === 0 && value.warned === 1))) return null;
|
|
67
80
|
counts.blocked += value.blocked; counts.warned += value.warned;
|
|
68
81
|
}
|
package/internal/guides/code.md
CHANGED
|
@@ -38,6 +38,8 @@ Apply these antislop criteria while writing and revising the affected code, with
|
|
|
38
38
|
- Name the operation or domain concept. Comments explain reasons and constraints rather than restating the code; errors identify the failed operation without leaking secrets.
|
|
39
39
|
- Remove introduced dead code, placeholder implementations and swallowed errors. Preserve intentional recovery and established compatibility layers.
|
|
40
40
|
- Test observable outcomes when tests are warranted. Do not add tests solely to mirror internal calls or prove cosmetic edits.
|
|
41
|
+
- A test must be able to fail on a wrong result. Never write a fake test: one that asserts whatever the code returns now without knowing it is right, recomputes the expected value with a copy of the code's logic, asserts only that a mock was called, reads source code as text instead of running it, or asserts something always true.
|
|
42
|
+
- After adding or changing tests, run `vibe mutate`: it plants small bugs in the changed source files with the project's mutation tool and lists the ones no test caught. For each: a boundary → add a case at that value; an untested branch → add an input that enters it; a constant such as a log message → leave it; the same behavior (an equivalent mutant) → skip it. The count is a sensor, not a target: never write a test only to raise it.
|
|
41
43
|
|
|
42
44
|
Patterns are evidence to examine, not banned syntax: a single-implementation interface, defensive check or TODO can be justified. Fix a specific behavior or maintenance problem, not an aesthetic preference. Do not read the full legacy skill or run its editorial chain as an automatic follow-up.
|
|
43
45
|
|
package/mcpb/server/index.js
CHANGED
|
@@ -17,6 +17,22 @@ import readline from 'node:readline';
|
|
|
17
17
|
const PROTOCOL = '2025-06-18';
|
|
18
18
|
const VERSION = process.env.VIBE_MCPB_VERSION || '0.0.0';
|
|
19
19
|
const project = process.env.VIBE_PROJECT_DIR || process.cwd();
|
|
20
|
+
const VIBE_CLIENT = 'claude-app';
|
|
21
|
+
const RUN_TIMEOUT_MS = 10 * 60 * 1000; // a check run may build and test the whole project
|
|
22
|
+
const TOOL_PREFIX = 'vibe_';
|
|
23
|
+
const INVALID_PARAMS = -32602;
|
|
24
|
+
const METHOD_NOT_FOUND = -32601;
|
|
25
|
+
const PARSE_ERROR = -32700;
|
|
26
|
+
const INSTALL_HINT = 'npm i -g @su-record/vibe';
|
|
27
|
+
const INSTRUCTIONS_TAIL = [
|
|
28
|
+
'Use the vibe tool with operation brief first; discover returns an internal operation schema.',
|
|
29
|
+
'The brief includes the personal FDE entry guidance; no extra model review by default.',
|
|
30
|
+
"Talk to the user in the user's language; write every record in English.",
|
|
31
|
+
].join(' ');
|
|
32
|
+
const ENTRY_DESCRIPTION = [
|
|
33
|
+
'Your personal FDE: project context, internal guidance, tools and skill extensions.',
|
|
34
|
+
'Use discover to inspect an operation before calling it.',
|
|
35
|
+
].join(' ');
|
|
20
36
|
|
|
21
37
|
const str = (description) => ({ type: 'string', description });
|
|
22
38
|
const TOOLS = [
|
|
@@ -73,8 +89,11 @@ function findVibe() {
|
|
|
73
89
|
const VIBE = findVibe();
|
|
74
90
|
|
|
75
91
|
function runVibe(args, input) {
|
|
76
|
-
if (!VIBE)
|
|
77
|
-
|
|
92
|
+
if (!VIBE) {
|
|
93
|
+
return { isError: true, text: `the vibe CLI was not found — install it with \`${INSTALL_HINT}\`, or set its path in the extension settings (vibe executable)` };
|
|
94
|
+
}
|
|
95
|
+
const env = { ...process.env, VIBE_CLIENT, PATH: `${path.dirname(VIBE)}${path.delimiter}${process.env.PATH || ''}` };
|
|
96
|
+
const r = spawnSync(VIBE, [...args, '--json'], { cwd: project, encoding: 'utf-8', input, timeout: RUN_TIMEOUT_MS, env, shell: process.platform === 'win32' });
|
|
78
97
|
if (r.error) return { isError: true, text: `could not run ${VIBE}: ${r.error.message}` };
|
|
79
98
|
const text = (r.stdout || '').trim() || (r.stderr || '').trim() || `exit ${r.status}`;
|
|
80
99
|
return { isError: r.status !== 0, text };
|
|
@@ -103,31 +122,40 @@ function handle(msg) {
|
|
|
103
122
|
if (msg.id === undefined) return; // notification
|
|
104
123
|
const { id, method, params = {} } = msg;
|
|
105
124
|
if (method === 'initialize') {
|
|
106
|
-
|
|
125
|
+
const fresh = fs.existsSync(`${project}/.vibe`) ? '' : ' (no .vibe yet — the first intent draft creates it)';
|
|
126
|
+
const instructions = `vibe project: ${project}${fresh} · CLI ${VIBE || `not found — ${INSTALL_HINT}`}. ${INSTRUCTIONS_TAIL}`;
|
|
127
|
+
return respond(id, { protocolVersion: params.protocolVersion || PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'vibe', version: VERSION }, instructions });
|
|
107
128
|
}
|
|
108
129
|
if (method === 'ping') return respond(id, {});
|
|
109
130
|
if (method === 'tools/list') {
|
|
110
|
-
|
|
131
|
+
const operations = ['discover', ...TOOLS.map(t => t.name.slice(TOOL_PREFIX.length))];
|
|
132
|
+
const properties = {
|
|
133
|
+
operation: { type: 'string', enum: operations },
|
|
134
|
+
arguments: { type: 'object', description: 'Arguments for the selected operation. discover accepts an optional operation name.' },
|
|
135
|
+
};
|
|
136
|
+
const inputSchema = { type: 'object', properties, required: ['operation'], additionalProperties: false };
|
|
137
|
+
return respond(id, { tools: [{ name: 'vibe', description: ENTRY_DESCRIPTION, inputSchema }] });
|
|
111
138
|
}
|
|
112
139
|
if (method === 'tools/call') {
|
|
113
140
|
const envelope = params.arguments || {};
|
|
114
141
|
if (params.name === 'vibe' && envelope.operation === 'discover') {
|
|
115
142
|
const selected = envelope.arguments?.operation;
|
|
116
|
-
const entries = selected ? TOOLS.filter(t => t.name ===
|
|
117
|
-
if (!entries.length) return fail(id,
|
|
118
|
-
const
|
|
143
|
+
const entries = selected ? TOOLS.filter(t => t.name === `${TOOL_PREFIX}${selected}`) : TOOLS;
|
|
144
|
+
if (!entries.length) return fail(id, INVALID_PARAMS, `unknown operation: ${selected}`);
|
|
145
|
+
const schema = (t) => ({ type: 'object', properties: t.args, required: t.required || [], additionalProperties: false });
|
|
146
|
+
const result = entries.map(t => ({ operation: t.name.slice(TOOL_PREFIX.length), description: t.description, ...(selected ? { inputSchema: schema(t) } : {}) }));
|
|
119
147
|
return respond(id, { content: [{ type: 'text', text: JSON.stringify(result) }] });
|
|
120
148
|
}
|
|
121
|
-
const name = params.name === 'vibe' ?
|
|
149
|
+
const name = params.name === 'vibe' ? `${TOOL_PREFIX}${envelope.operation}` : params.name;
|
|
122
150
|
const tool = TOOLS.find((t) => t.name === name);
|
|
123
|
-
if (!tool) return fail(id,
|
|
151
|
+
if (!tool) return fail(id, INVALID_PARAMS, `unknown tool: ${params.name}`);
|
|
124
152
|
const a = params.name === 'vibe' ? envelope.arguments || {} : envelope;
|
|
125
153
|
const invalid = invalidArguments(tool, a);
|
|
126
|
-
if (invalid) return fail(id,
|
|
154
|
+
if (invalid) return fail(id, INVALID_PARAMS, invalid);
|
|
127
155
|
const r = runVibe(tool.cmd(a), tool.stdin ? tool.stdin(a) : undefined);
|
|
128
156
|
return respond(id, { content: [{ type: 'text', text: r.text }], isError: r.isError });
|
|
129
157
|
}
|
|
130
|
-
return fail(id,
|
|
158
|
+
return fail(id, METHOD_NOT_FOUND, `method not found: ${method}`);
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
readline.createInterface({ input: process.stdin }).on('line', (line) => {
|
|
@@ -135,6 +163,6 @@ readline.createInterface({ input: process.stdin }).on('line', (line) => {
|
|
|
135
163
|
try {
|
|
136
164
|
handle(JSON.parse(line));
|
|
137
165
|
} catch (error) {
|
|
138
|
-
fail(null,
|
|
166
|
+
fail(null, PARSE_ERROR, `parse error: ${error.message}`);
|
|
139
167
|
}
|
|
140
168
|
});
|
package/package.json
CHANGED