@su-record/vibe 4.2.35 → 4.2.36

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.
Files changed (217) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/dist/cli/common.js +4 -1
  5. package/dist/cli/common.js.map +1 -1
  6. package/dist/cli/heldout.js +10 -4
  7. package/dist/cli/heldout.js.map +1 -1
  8. package/dist/cli/human.js +8 -4
  9. package/dist/cli/human.js.map +1 -1
  10. package/dist/cli/internal.js +37 -24
  11. package/dist/cli/internal.js.map +1 -1
  12. package/dist/cli/ledger.js +67 -0
  13. package/dist/cli/ledger.js.map +1 -0
  14. package/dist/cli/map.js +5 -3
  15. package/dist/cli/map.js.map +1 -1
  16. package/dist/cli/memory.js +104 -74
  17. package/dist/cli/memory.js.map +1 -1
  18. package/dist/cli/risks.js +20 -3
  19. package/dist/cli/risks.js.map +1 -1
  20. package/dist/cli/session.js +4 -2
  21. package/dist/cli/session.js.map +1 -1
  22. package/dist/cli/setup.js +44 -13
  23. package/dist/cli/setup.js.map +1 -1
  24. package/dist/cli/work.js +73 -21
  25. package/dist/cli/work.js.map +1 -1
  26. package/dist/cli.js +18 -6
  27. package/dist/cli.js.map +1 -1
  28. package/dist/core/analyze.js +21 -7
  29. package/dist/core/analyze.js.map +1 -1
  30. package/dist/core/artifact-paths.js +14 -3
  31. package/dist/core/artifact-paths.js.map +1 -1
  32. package/dist/core/capabilities.js +69 -15
  33. package/dist/core/capabilities.js.map +1 -1
  34. package/dist/core/check-authorization.js +37 -0
  35. package/dist/core/check-authorization.js.map +1 -0
  36. package/dist/core/check-process.js +12 -4
  37. package/dist/core/check-process.js.map +1 -1
  38. package/dist/core/check-report.js +66 -0
  39. package/dist/core/check-report.js.map +1 -0
  40. package/dist/core/check.js +59 -71
  41. package/dist/core/check.js.map +1 -1
  42. package/dist/core/checks/changed.js +20 -6
  43. package/dist/core/checks/changed.js.map +1 -1
  44. package/dist/core/checks/eval.js +35 -10
  45. package/dist/core/checks/eval.js.map +1 -1
  46. package/dist/core/checks/file.js +26 -23
  47. package/dist/core/checks/file.js.map +1 -1
  48. package/dist/core/checks/fileRules.js +42 -16
  49. package/dist/core/checks/fileRules.js.map +1 -1
  50. package/dist/core/checks/http.js +35 -34
  51. package/dist/core/checks/http.js.map +1 -1
  52. package/dist/core/checks/packs.js +1 -1
  53. package/dist/core/checks/packs.js.map +1 -1
  54. package/dist/core/checks/review.js +75 -40
  55. package/dist/core/checks/review.js.map +1 -1
  56. package/dist/core/checks/run.js +5 -2
  57. package/dist/core/checks/run.js.map +1 -1
  58. package/dist/core/checks/schema.js +25 -0
  59. package/dist/core/checks/schema.js.map +1 -0
  60. package/dist/core/checks/source.js +11 -6
  61. package/dist/core/checks/source.js.map +1 -1
  62. package/dist/core/config.js +9 -2
  63. package/dist/core/config.js.map +1 -1
  64. package/dist/core/consent.js +24 -16
  65. package/dist/core/consent.js.map +1 -1
  66. package/dist/core/context-notes.js +15 -7
  67. package/dist/core/context-notes.js.map +1 -1
  68. package/dist/core/context.js +24 -12
  69. package/dist/core/context.js.map +1 -1
  70. package/dist/core/conventions.js +22 -9
  71. package/dist/core/conventions.js.map +1 -1
  72. package/dist/core/docs/cfb.js +81 -45
  73. package/dist/core/docs/cfb.js.map +1 -1
  74. package/dist/core/docs/html.js +14 -3
  75. package/dist/core/docs/html.js.map +1 -1
  76. package/dist/core/docs/hwp.js +44 -22
  77. package/dist/core/docs/hwp.js.map +1 -1
  78. package/dist/core/docs/office.js +20 -10
  79. package/dist/core/docs/office.js.map +1 -1
  80. package/dist/core/docs/pdf.js +10 -4
  81. package/dist/core/docs/pdf.js.map +1 -1
  82. package/dist/core/docs/read.js +32 -24
  83. package/dist/core/docs/read.js.map +1 -1
  84. package/dist/core/docs/zip.js +32 -15
  85. package/dist/core/docs/zip.js.map +1 -1
  86. package/dist/core/evidence.js +17 -7
  87. package/dist/core/evidence.js.map +1 -1
  88. package/dist/core/failure-message.js +35 -13
  89. package/dist/core/failure-message.js.map +1 -1
  90. package/dist/core/failure.js +49 -14
  91. package/dist/core/failure.js.map +1 -1
  92. package/dist/core/github.js +3 -1
  93. package/dist/core/github.js.map +1 -1
  94. package/dist/core/handoff.js +20 -7
  95. package/dist/core/handoff.js.map +1 -1
  96. package/dist/core/heldout-run.js +6 -3
  97. package/dist/core/heldout-run.js.map +1 -1
  98. package/dist/core/heldout.js +24 -13
  99. package/dist/core/heldout.js.map +1 -1
  100. package/dist/core/inbox.js +3 -1
  101. package/dist/core/inbox.js.map +1 -1
  102. package/dist/core/inputs.js +6 -3
  103. package/dist/core/inputs.js.map +1 -1
  104. package/dist/core/inspect.js +61 -20
  105. package/dist/core/inspect.js.map +1 -1
  106. package/dist/core/intent.js +35 -13
  107. package/dist/core/intent.js.map +1 -1
  108. package/dist/core/knowledge-find.js +31 -12
  109. package/dist/core/knowledge-find.js.map +1 -1
  110. package/dist/core/knowledge.js +22 -12
  111. package/dist/core/knowledge.js.map +1 -1
  112. package/dist/core/lang.js +5 -2
  113. package/dist/core/lang.js.map +1 -1
  114. package/dist/core/ledger.js +44 -20
  115. package/dist/core/ledger.js.map +1 -1
  116. package/dist/core/locations.js +12 -0
  117. package/dist/core/locations.js.map +1 -0
  118. package/dist/core/map/imports.js +20 -9
  119. package/dist/core/map/imports.js.map +1 -1
  120. package/dist/core/map/index.js +28 -13
  121. package/dist/core/map/index.js.map +1 -1
  122. package/dist/core/map/symbols.js +27 -12
  123. package/dist/core/map/symbols.js.map +1 -1
  124. package/dist/core/output-capture.js +7 -4
  125. package/dist/core/output-capture.js.map +1 -1
  126. package/dist/core/paths.js +2 -1
  127. package/dist/core/paths.js.map +1 -1
  128. package/dist/core/performance.js +43 -17
  129. package/dist/core/performance.js.map +1 -1
  130. package/dist/core/profile.js +6 -3
  131. package/dist/core/profile.js.map +1 -1
  132. package/dist/core/reader.js +32 -16
  133. package/dist/core/reader.js.map +1 -1
  134. package/dist/core/readerSession.js +34 -14
  135. package/dist/core/readerSession.js.map +1 -1
  136. package/dist/core/recent.js +20 -6
  137. package/dist/core/recent.js.map +1 -1
  138. package/dist/core/regress.js +21 -12
  139. package/dist/core/regress.js.map +1 -1
  140. package/dist/core/repair.js +22 -4
  141. package/dist/core/repair.js.map +1 -1
  142. package/dist/core/research-queries.js +85 -0
  143. package/dist/core/research-queries.js.map +1 -0
  144. package/dist/core/research.js +89 -88
  145. package/dist/core/research.js.map +1 -1
  146. package/dist/core/results.js +15 -6
  147. package/dist/core/results.js.map +1 -1
  148. package/dist/core/risk-signals.js +53 -15
  149. package/dist/core/risk-signals.js.map +1 -1
  150. package/dist/core/run-id.js +10 -6
  151. package/dist/core/run-id.js.map +1 -1
  152. package/dist/core/scenarios.js +91 -60
  153. package/dist/core/scenarios.js.map +1 -1
  154. package/dist/core/session.js +3 -2
  155. package/dist/core/session.js.map +1 -1
  156. package/dist/core/size.js +28 -8
  157. package/dist/core/size.js.map +1 -1
  158. package/dist/core/skills.js +66 -28
  159. package/dist/core/skills.js.map +1 -1
  160. package/dist/core/skillsmp.js +26 -12
  161. package/dist/core/skillsmp.js.map +1 -1
  162. package/dist/core/source-basis.js +21 -9
  163. package/dist/core/source-basis.js.map +1 -1
  164. package/dist/core/status.js +26 -13
  165. package/dist/core/status.js.map +1 -1
  166. package/dist/core/table.js +8 -1
  167. package/dist/core/table.js.map +1 -1
  168. package/dist/core/tokens.js +20 -10
  169. package/dist/core/tokens.js.map +1 -1
  170. package/dist/core/tree.js +26 -16
  171. package/dist/core/tree.js.map +1 -1
  172. package/dist/core/verification-plan.js +24 -8
  173. package/dist/core/verification-plan.js.map +1 -1
  174. package/dist/core/verification.js +14 -7
  175. package/dist/core/verification.js.map +1 -1
  176. package/dist/core/view.js +93 -44
  177. package/dist/core/view.js.map +1 -1
  178. package/dist/install/global.js +37 -18
  179. package/dist/install/global.js.map +1 -1
  180. package/dist/install/hooks.js +17 -8
  181. package/dist/install/hooks.js.map +1 -1
  182. package/dist/install/manifest.js +11 -0
  183. package/dist/install/manifest.js.map +1 -0
  184. package/dist/install/mcpb.js +52 -23
  185. package/dist/install/mcpb.js.map +1 -1
  186. package/dist/install/plugin.js +73 -33
  187. package/dist/install/plugin.js.map +1 -1
  188. package/dist/install/project.js +8 -7
  189. package/dist/install/project.js.map +1 -1
  190. package/dist/install/register.js +83 -48
  191. package/dist/install/register.js.map +1 -1
  192. package/dist/install/tree.js +27 -11
  193. package/dist/install/tree.js.map +1 -1
  194. package/dist/install/update.js +46 -20
  195. package/dist/install/update.js.map +1 -1
  196. package/hooks/code-map.cjs +22 -9
  197. package/hooks/journal.cjs +18 -16
  198. package/hooks/knowledge-auto.cjs +55 -21
  199. package/hooks/knowledge-journal.cjs +47 -20
  200. package/hooks/knowledge-projects.cjs +50 -21
  201. package/hooks/knowledge-recall.cjs +16 -6
  202. package/hooks/knowledge-upkeep.cjs +22 -5
  203. package/hooks/locations.cjs +35 -0
  204. package/hooks/notify.js +52 -24
  205. package/hooks/obsidian.cjs +17 -7
  206. package/hooks/private-store.cjs +25 -10
  207. package/hooks/retest.cjs +39 -16
  208. package/hooks/session-context.cjs +30 -13
  209. package/hooks/session-files.cjs +44 -21
  210. package/hooks/session-repair.cjs +26 -11
  211. package/hooks/session-state.cjs +78 -38
  212. package/hooks/session.js +19 -8
  213. package/hooks/slice-command.cjs +38 -17
  214. package/hooks/slice-guard.cjs +20 -6
  215. package/hooks/slice-store.cjs +25 -12
  216. package/mcpb/server/index.js +40 -12
  217. 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
- function read(file, limit = 65536) {
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, 'card.md'), 4096).trim();
23
- const homeCard = client === 'claude' ? path.join(home, '.claude', 'CLAUDE.md') : path.join(home, '.codex', 'AGENTS.md');
24
- const homeOwns = read(homeCard).includes('<!-- vibe:start -->');
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) process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SubagentStart', additionalContext: card } })}\n`);
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 status = view.status === 'bound' ? 'session status only; run vibe state for the procedure' : `${view.status}; use explicit vibe session bind in the intended worktree`;
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
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text }, ...(line ? { systemMessage: line } : {}) })}\n`);
50
+ emit('SessionStart', text, line ? { systemMessage: line } : {});
@@ -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 > 65536) return null;
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 > 2048) return null;
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 => /^--files(?:-with-matches|-without-match)?$|^-(?!-)[A-Za-z]*[lL][A-Za-z]*$/.test(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, ['-e', '--regexp', '-f', '--file', '-g', '--glob', '-t', '--type', '-T', '--type-not', '-m', '--max-count', '--max-columns', '--max-depth', '-A', '-B', '-C']);
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
- const values = {
75
- nl: ['-b', '-d', '-f', '-h', '-i', '-l', '-n', '-s', '-v', '--body-numbering', '--section-delimiter', '--footer-numbering', '--header-numbering', '--line-increment', '--join-blank-lines', '--number-format', '--number-separator', '--starting-line-number'],
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, ['-n', '-c', '--lines', '--bytes', '--sleep-interval']), slice: true };
87
- if (name === 'cut') return { files: operands(args, ['-b', '-c', '-f', '-d', '--bytes', '--characters', '--fields', '--delimiter', '--output-delimiter']), slice: true };
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, ['-e', '-f', '--expression', '--file', '-F', '-v']);
109
+ const files = operands(args, SCRIPT_VALUED);
91
110
  if (!explicit) files.shift();
92
- const slice = name === 'awk' ? args.some(word => /\bNR\b|\bprint\b/.test(word)) : args.some(word => /^-[^-]*n/.test(word) || word === '--quiet' || word === '--silent');
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 > 1) return { unavailable: true, files: [] };
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, 256) };
140
+ return { unavailable: false, files: [...found].slice(0, MAX_TARGETS) };
120
141
  }
121
142
  module.exports = { sliceTargets };
@@ -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 (relative === '.vibe' || relative === '.vibe/intent.md' || /(?:^|\/)scenarios\.ya?ml$/i.test(relative) || /(?:^|\/)specs?(?:\/|$)/i.test(relative)) return true;
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 (/\.(?:[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.test(file)) return true;
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 `Open the selected files in full with ${tool}. Read fewer files from vibe state; use grep/rg/find only to locate files or lines. For over 400 lines when not editing, use vibe read --ask.`;
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
- return { decision, unavailable, recorded, message: `[vibe] ${decision === 'block' ? 'Partial reading of a protected file is blocked.' : 'Partial source/document reading can omit requirements.'} ${guidance(client)}${unavailable ? ' Read-target mirror or count storage unavailable.' : ''}` };
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 };
@@ -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 || !/^[a-f0-9]{16,64}$/.test(input.intentHash) || !Array.isArray(input.files) || input.files.length > 2048 || input.files.some(file => typeof file !== 'string' || file.length >= 4096)) return false;
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) > 131072) return false;
22
- writePrivate(root, `read-targets-${digest(root)}.json`, body, explicitHome(env));
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, `read-targets-${digest(root)}.json`, 131072);
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, '.vibe/state.json'));
31
- if (saved.schemaVersion !== 1 || saved.root !== root || saved.intentHash !== state.intentHash || !Array.isArray(saved.files) || saved.files.length > 2048 || saved.files.some(file => typeof file !== 'string' || !path.isAbsolute(file))) return { available: false, files: [] };
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(12).toString('hex')}.json`, JSON.stringify(counts));
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), 4096, home);
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), 4096, home));
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 > 32768) return null;
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, 128, home));
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
  }
@@ -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) return { isError: true, text: 'the vibe CLI was not found — install it with `npm i -g @su-record/vibe`, or set its path in the extension settings (vibe executable)' };
77
- const r = spawnSync(VIBE, [...args, '--json'], { cwd: project, encoding: 'utf-8', input, timeout: 600000, env: { ...process.env, VIBE_CLIENT: 'claude-app', PATH: `${path.dirname(VIBE)}${path.delimiter}${process.env.PATH || ''}` }, shell: process.platform === 'win32' });
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
- return respond(id, { protocolVersion: params.protocolVersion || PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'vibe', version: VERSION }, instructions: `vibe project: ${project}${fs.existsSync(`${project}/.vibe`) ? '' : ' (no .vibe yet — the first intent draft creates it)'} · CLI ${VIBE || 'not found — npm i -g @su-record/vibe'}. Use the vibe tool with operation brief first; discover returns an internal operation schema. The brief includes the personal FDE entry guidance; no extra model review by default. Talk to the user in the user's language; write every record in English.` });
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
- return respond(id, { tools: [{ name: 'vibe', description: 'Your personal FDE: project context, internal guidance, tools and skill extensions. Use discover to inspect an operation before calling it.', inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['discover', ...TOOLS.map(t => t.name.slice(5))] }, arguments: { type: 'object', description: 'Arguments for the selected operation. discover accepts an optional operation name.' } }, required: ['operation'], additionalProperties: false } }] });
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 === `vibe_${selected}`) : TOOLS;
117
- if (!entries.length) return fail(id, -32602, `unknown operation: ${selected}`);
118
- const result = entries.map(t => ({ operation: t.name.slice(5), description: t.description, ...(selected ? { inputSchema: { type: 'object', properties: t.args, required: t.required || [], additionalProperties: false } } : {}) }));
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' ? `vibe_${envelope.operation}` : params.name;
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, -32602, `unknown tool: ${params.name}`);
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, -32602, invalid);
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, -32601, `method not found: ${method}`);
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, -32700, `parse error: ${error.message}`);
166
+ fail(null, PARSE_ERROR, `parse error: ${error.message}`);
139
167
  }
140
168
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "4.2.35",
3
+ "version": "4.2.36",
4
4
  "description": "Your personal FDE in Claude and Codex. Understand the work, use the right tools, deliver and retain useful context. One entry: vibe.",
5
5
  "type": "module",
6
6
  "bin": {