@su-record/vibe 4.2.19 → 4.2.21
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 -3
- package/dist/cli/common.js.map +1 -1
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/memory.js +27 -13
- package/dist/cli/memory.js.map +1 -1
- package/dist/cli/setup.js +13 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/knowledge-lint.js +4 -83
- package/dist/core/knowledge-lint.js.map +1 -1
- package/dist/core/knowledge.js +14 -4
- package/dist/core/knowledge.js.map +1 -1
- package/dist/core/retest.js +19 -0
- package/dist/core/retest.js.map +1 -0
- package/dist/install/plugin.js +1 -1
- package/dist/install/plugin.js.map +1 -1
- package/hooks/journal.cjs +18 -9
- package/hooks/knowledge-auto.cjs +8 -3
- package/hooks/knowledge-journal.cjs +47 -5
- package/hooks/knowledge-lint.cjs +150 -0
- package/hooks/knowledge-projects.cjs +12 -1
- package/hooks/knowledge-upkeep.cjs +49 -0
- package/hooks/notify.js +17 -6
- package/hooks/retest.cjs +145 -0
- package/internal/guides/wiki.md +7 -2
- package/package.json +1 -1
package/dist/core/knowledge.js
CHANGED
|
@@ -15,19 +15,25 @@ export function knowledgeDir(root) {
|
|
|
15
15
|
const defaultHome = () => process.env['VIBE_HOME_DIR'] ?? os.homedir();
|
|
16
16
|
/** Conversation turns without a file change before a knowledge turn; 0 keeps only the file-change trigger. */
|
|
17
17
|
export const DEFAULT_KNOWLEDGE_TURNS = 5;
|
|
18
|
+
/** Changed wiki pages, and days with a changed page, before a knowledge turn also reviews them; 0 pages turns review off. */
|
|
19
|
+
export const DEFAULT_REVIEW_PAGES = 10;
|
|
20
|
+
export const DEFAULT_REVIEW_DAYS = 14;
|
|
18
21
|
export function knowledgeSettingsFile(home = defaultHome()) {
|
|
19
22
|
return path.join(home, '.config', 'vibe', 'knowledge.json');
|
|
20
23
|
}
|
|
21
24
|
export function knowledgeSettings(home = defaultHome()) {
|
|
22
25
|
const raw = readJson(knowledgeSettingsFile(home)) ?? {};
|
|
23
|
-
const { dir, turns, exclude } = raw;
|
|
26
|
+
const { dir, turns, exclude, review, reviewDays } = raw;
|
|
27
|
+
const count = (value, fallback) => (Number.isInteger(value) && value >= 0 ? value : fallback);
|
|
24
28
|
return {
|
|
25
29
|
auto: raw['auto'] === true,
|
|
26
30
|
...(typeof dir === 'string' && path.isAbsolute(dir) ? { dir } : {}),
|
|
27
|
-
turns:
|
|
31
|
+
turns: count(turns, DEFAULT_KNOWLEDGE_TURNS),
|
|
28
32
|
skills: raw['skills'] === 'create' ? 'create' : 'propose',
|
|
29
33
|
exclude: Array.isArray(exclude) ? exclude.filter((e) => typeof e === 'string' && path.isAbsolute(e)) : [],
|
|
30
34
|
journal: raw['journal'] !== false,
|
|
35
|
+
review: count(review, DEFAULT_REVIEW_PAGES),
|
|
36
|
+
reviewDays: count(reviewDays, DEFAULT_REVIEW_DAYS),
|
|
31
37
|
};
|
|
32
38
|
}
|
|
33
39
|
function absolute(flag, value) {
|
|
@@ -38,8 +44,10 @@ function absolute(flag, value) {
|
|
|
38
44
|
/** Apply the person's change; a folder must be absolute, and what the change leaves out is kept. */
|
|
39
45
|
export function setKnowledgeSettings(change, home = defaultHome()) {
|
|
40
46
|
const current = knowledgeSettings(home);
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
for (const [flag, value] of [['turns', change.turns], ['review', change.review], ['review-days', change.reviewDays]]) {
|
|
48
|
+
if (value !== undefined && !(Number.isInteger(value) && value >= 0))
|
|
49
|
+
throw usage(`--${flag} takes a whole number, 0 or more`);
|
|
50
|
+
}
|
|
43
51
|
if (change.skills !== undefined && !['propose', 'create'].includes(change.skills))
|
|
44
52
|
throw usage('--skills takes propose | create');
|
|
45
53
|
if (change.journal !== undefined && !['on', 'off'].includes(change.journal))
|
|
@@ -53,6 +61,8 @@ export function setKnowledgeSettings(change, home = defaultHome()) {
|
|
|
53
61
|
skills: change.skills ?? current.skills,
|
|
54
62
|
exclude: [...new Set([...current.exclude, ...(exclude ? [exclude] : [])])].filter(folder => folder !== include),
|
|
55
63
|
journal: change.journal === undefined ? current.journal : change.journal === 'on',
|
|
64
|
+
review: change.review ?? current.review,
|
|
65
|
+
reviewDays: change.reviewDays ?? current.reviewDays,
|
|
56
66
|
};
|
|
57
67
|
writeJson(knowledgeSettingsFile(home), next);
|
|
58
68
|
return next;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knowledge.js","sourceRoot":"","sources":["../../src/core/knowledge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE9D,2FAA2F;AAC3F,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;AAG/E,8GAA8G;AAC9G,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"knowledge.js","sourceRoot":"","sources":["../../src/core/knowledge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { usage } from './errors.js';
|
|
4
|
+
import { writeJson } from './store.js';
|
|
5
|
+
// Shared with the Bash hooks, which keep the gate's memory; the CLI reads it and writes the person's setting.
|
|
6
|
+
const gate = createRequire(import.meta.url)('../../hooks/retest.cjs');
|
|
7
|
+
const defaultHome = () => process.env['VIBE_HOME_DIR'] ?? os.homedir();
|
|
8
|
+
/** What the retest gate did in this work tree over 30 days, with the person's setting. */
|
|
9
|
+
export function retestSummary(cwd, home = defaultHome()) {
|
|
10
|
+
return gate.summary(cwd, home);
|
|
11
|
+
}
|
|
12
|
+
export function setRetestMode(value, home = defaultHome()) {
|
|
13
|
+
if (!gate.MODES.includes(value))
|
|
14
|
+
throw usage(`retest takes ${gate.MODES.join(' | ')}`);
|
|
15
|
+
writeJson(gate.settingsFile(home), { mode: value });
|
|
16
|
+
return gate.mode(home);
|
|
17
|
+
}
|
|
18
|
+
export const { testCommand } = gate;
|
|
19
|
+
//# sourceMappingURL=retest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retest.js","sourceRoot":"","sources":["../../src/core/retest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAWvC,8GAA8G;AAC9G,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,wBAAwB,CAMnE,CAAC;AAEF,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;AAE/E,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAe,WAAW,EAAE;IACrE,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,OAAe,WAAW,EAAE;IACvE,IAAI,CAAE,IAAI,CAAC,KAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,KAAK,CAAC,gBAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrG,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC"}
|
package/dist/install/plugin.js
CHANGED
|
@@ -12,7 +12,7 @@ import { pluginTree } from './tree.js';
|
|
|
12
12
|
* manifest, the single entry skill and the notification hooks.
|
|
13
13
|
*/
|
|
14
14
|
const SKILL_NAMES = ['vibe'];
|
|
15
|
-
const HOOK_FILES = ['notify.js', 'session.js', 'private-store.cjs', 'session-files.cjs', 'session-state.cjs', 'session-context.cjs', 'session-repair.cjs', 'knowledge-auto.cjs', 'knowledge-projects.cjs', 'knowledge-journal.cjs', 'journal.cjs', 'obsidian.cjs', 'slice-command.cjs', 'slice-store.cjs', 'slice-guard.cjs'];
|
|
15
|
+
const HOOK_FILES = ['notify.js', 'session.js', 'private-store.cjs', 'session-files.cjs', 'session-state.cjs', 'session-context.cjs', 'session-repair.cjs', 'knowledge-auto.cjs', 'knowledge-projects.cjs', 'knowledge-lint.cjs', 'knowledge-upkeep.cjs', 'knowledge-journal.cjs', 'retest.cjs', 'journal.cjs', 'obsidian.cjs', 'slice-command.cjs', 'slice-store.cjs', 'slice-guard.cjs'];
|
|
16
16
|
export const MARKETPLACE_NAME = 'vibe-local';
|
|
17
17
|
/** The tree lives under ~/.config/vibe so that `.vibe/` keeps one meaning: a project's records. ≤ 4.1.7 put it at ~/.vibe/plugin. */
|
|
18
18
|
export function pluginPaths(home = os.homedir()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/install/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,CAAC,MAAM,CAAU,CAAC;AACtC,MAAM,UAAU,GAAG,CAAC,WAAW,EAAE,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/install/plugin.ts"],"names":[],"mappings":"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"}
|
package/hooks/journal.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// UserPromptSubmit (`prompt`) and PostToolUse(Bash) (`tool`): one line in the session-knowledge journal, printing
|
|
3
|
-
// nothing
|
|
4
|
-
// is off a prompt or a shell call
|
|
3
|
+
// nothing, and the retest gate's memory of a passing test command. It reads the person's settings and the command
|
|
4
|
+
// before loading anything else, so while session knowledge or its journal is off a prompt or a non-test shell call
|
|
5
|
+
// costs only the start of node.
|
|
5
6
|
const fs = require('node:fs');
|
|
6
7
|
const os = require('node:os');
|
|
7
8
|
const path = require('node:path');
|
|
@@ -28,11 +29,19 @@ function npmOwns() {
|
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
let
|
|
32
|
-
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
let payload = null;
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
35
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) payload = value;
|
|
36
|
+
} catch { /* no payload */ }
|
|
37
|
+
// The retest gate remembers a passing test whatever the journal setting; it loads only for a test command.
|
|
38
|
+
const testLike = kind === 'tool' && /\b(?:test|check|vitest|jest|mocha|ava|pytest|unittest|rspec|phpunit|nextest|verify|cypress|playwright)\b/.test(JSON.stringify(payload?.tool_input?.command ?? ''));
|
|
39
|
+
const journaling = payload !== null && journalOn();
|
|
40
|
+
if ((testLike || journaling) && !(process.argv.includes('--plugin') && npmOwns())) {
|
|
41
|
+
if (testLike) {
|
|
42
|
+
try { require('./retest.cjs').after(payload, home); } catch { /* nothing remembered */ }
|
|
43
|
+
}
|
|
44
|
+
if (journaling) {
|
|
45
|
+
try { require('./knowledge-auto.cjs').observe(kind, payload, home); } catch { /* the work goes on without a journal line */ }
|
|
46
|
+
}
|
|
38
47
|
}
|
package/hooks/knowledge-auto.cjs
CHANGED
|
@@ -11,7 +11,8 @@ const { inside, readPrivate, writePrivate } = require('./private-store.cjs');
|
|
|
11
11
|
const { optional, digest, revision, projectFingerprint } = require('./session-files.cjs');
|
|
12
12
|
const { identity, hostRoot } = require('./session-state.cjs');
|
|
13
13
|
const { connectVault } = require('./obsidian.cjs');
|
|
14
|
-
const { projectName, linkProject, indexProject } = require('./knowledge-projects.cjs');
|
|
14
|
+
const { projectName, linkProject, indexProject, projectPage } = require('./knowledge-projects.cjs');
|
|
15
|
+
const upkeep = require('./knowledge-upkeep.cjs');
|
|
15
16
|
const journal = require('./knowledge-journal.cjs');
|
|
16
17
|
|
|
17
18
|
const defaultHome = () => process.env.VIBE_HOME_DIR || os.homedir();
|
|
@@ -28,6 +29,8 @@ function settings(home) {
|
|
|
28
29
|
skills: raw.skills === 'create' ? 'create' : 'propose',
|
|
29
30
|
exclude: Array.isArray(raw.exclude) ? raw.exclude.filter(e => typeof e === 'string' && path.isAbsolute(e)) : [],
|
|
30
31
|
journal: raw.journal !== false,
|
|
32
|
+
review: Number.isInteger(raw.review) && raw.review >= 0 ? raw.review : 10,
|
|
33
|
+
reviewDays: Number.isInteger(raw.reviewDays) && raw.reviewDays >= 0 ? raw.reviewDays : 14,
|
|
31
34
|
};
|
|
32
35
|
} catch { return { auto: false }; }
|
|
33
36
|
}
|
|
@@ -105,7 +108,7 @@ const REASON = (places, skills, observed) => `[vibe] Knowledge turn: the user tu
|
|
|
105
108
|
|
|
106
109
|
/** Stop: after a changed turn, or `turns` conversation turns, log the fact and give the model one knowledge turn. */
|
|
107
110
|
function stop(payload, home = defaultHome()) {
|
|
108
|
-
const { auto, dir, turns: every, skills, exclude, journal: on } = settings(home);
|
|
111
|
+
const { auto, dir, turns: every, skills, exclude, journal: on, review, reviewDays } = settings(home);
|
|
109
112
|
if (!auto || payload.stop_hook_active === true) return {};
|
|
110
113
|
try {
|
|
111
114
|
const now = snapshot(payload, home, exclude);
|
|
@@ -118,11 +121,13 @@ function stop(payload, home = defaultHome()) {
|
|
|
118
121
|
}
|
|
119
122
|
fs.mkdirSync(dir, { recursive: true });
|
|
120
123
|
const name = projectName(now.root, home);
|
|
124
|
+
projectPage(dir, name, now.root);
|
|
121
125
|
fs.appendFileSync(path.join(dir, 'log.md'), logLine(now.root, changed ? 'files changed' : 'conversation', name));
|
|
126
|
+
const wiki = upkeep.tidy(dir, home) + upkeep.review(dir, home, review, reviewDays);
|
|
122
127
|
// A wiki inside the project changes the files it was just compared with; look again so that is not seen as work.
|
|
123
128
|
const files = inside(now.root, dir) ? projectFingerprint(now.root) : now.files;
|
|
124
129
|
writePrivate(now.root, entry(now.root, now.id), JSON.stringify({ files, turns: 0 }), home);
|
|
125
|
-
return { decision: 'block', reason: REASON(PLACES(dir, now.root, name), skills, on ? observed(now, home) : '') };
|
|
130
|
+
return { decision: 'block', reason: REASON(PLACES(dir, now.root, name), skills, on ? observed(now, home) : '') + wiki };
|
|
126
131
|
} catch { return {}; }
|
|
127
132
|
}
|
|
128
133
|
|
|
@@ -28,10 +28,32 @@ const CORRECTION = /^\s*(?:no\b|nope\b|wrong\b|not that\b|that'?s not\b|don'?t\b
|
|
|
28
28
|
// Tools whose first word says little; their subcommand is the command's meaning.
|
|
29
29
|
const WITH_SUBCOMMAND = new Set(['git', 'npm', 'npx', 'pnpm', 'yarn', 'gh', 'docker', 'kubectl', 'cargo', 'go', 'make', 'pip', 'pip3', 'uv', 'vibe', 'node', 'python', 'python3', 'deno', 'bun', 'terraform', 'aws', 'gcloud', 'az']);
|
|
30
30
|
|
|
31
|
+
/** The commands of a shell line, split at `&&`, `||`, `;`, `|` and line ends outside quotes. */
|
|
32
|
+
function segments(command) {
|
|
33
|
+
const text = Array.isArray(command) ? command.join(' ') : String(command ?? '');
|
|
34
|
+
const out = [];
|
|
35
|
+
let current = '', quote = null;
|
|
36
|
+
for (let i = 0; i < text.length; i++) {
|
|
37
|
+
const c = text[i];
|
|
38
|
+
if (c === '\\' && quote !== "'") { current += c + (text[++i] ?? ''); continue; }
|
|
39
|
+
if (quote) { if (c === quote) quote = null; current += c; continue; }
|
|
40
|
+
if (c === '"' || c === "'") { quote = c; current += c; continue; }
|
|
41
|
+
const pair = text.slice(i, i + 2);
|
|
42
|
+
if (pair === '&&' || pair === '||' || c === ';' || c === '|' || c === '\n') {
|
|
43
|
+
out.push(current); current = '';
|
|
44
|
+
if (pair === '&&' || pair === '||') i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
current += c;
|
|
48
|
+
}
|
|
49
|
+
out.push(current);
|
|
50
|
+
return out.map(s => s.trim()).filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/** The command words of a shell line, `git push` or `npm run`, without arguments, values or paths. */
|
|
32
54
|
function commandHeads(command) {
|
|
33
55
|
const heads = [];
|
|
34
|
-
for (const segment of
|
|
56
|
+
for (const segment of segments(command)) {
|
|
35
57
|
const words = segment.trim().split(/\s+/).filter(word => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
|
|
36
58
|
const first = (words[0] ?? '').split('/').pop();
|
|
37
59
|
if (!/^[A-Za-z][\w.+-]{0,40}$/.test(first)) continue;
|
|
@@ -42,12 +64,32 @@ function commandHeads(command) {
|
|
|
42
64
|
return heads.slice(0, 4);
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
const EXIT_TEXT = /(?:exit code|exited with code|exit status)[:\s]+(-?\d+)/i;
|
|
68
|
+
const exitText = text => { const m = EXIT_TEXT.exec(text); return m ? Number(m[1]) === 0 : null; };
|
|
69
|
+
|
|
45
70
|
/** Whether a shell call worked, from the fields clients report; null when the response does not say. */
|
|
46
71
|
function outcome(response) {
|
|
72
|
+
if (typeof response === 'string') return exitText(response);
|
|
47
73
|
if (!response || typeof response !== 'object') return null;
|
|
48
|
-
for (const
|
|
74
|
+
for (const source of [response, response.metadata ?? {}]) {
|
|
75
|
+
for (const key of ['exit_code', 'exitCode', 'returnCode', 'status']) if (Number.isInteger(source[key])) return source[key] === 0;
|
|
76
|
+
}
|
|
49
77
|
if (response.interrupted === true || response.is_error === true || response.isError === true) return false;
|
|
50
|
-
return null;
|
|
78
|
+
return typeof response.output === 'string' ? exitText(response.output) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clientOf(payload, env = process.env) {
|
|
82
|
+
const transcript = typeof payload.transcript_path === 'string' ? payload.transcript_path.replaceAll('\\', '/') : '';
|
|
83
|
+
if (transcript.includes('/.codex/')) return 'codex';
|
|
84
|
+
if (transcript.includes('/.claude/')) return 'claude';
|
|
85
|
+
if (env.VIBE_CLIENT) return ['codex', 'chatgpt'].includes(env.VIBE_CLIENT) ? 'codex' : 'claude';
|
|
86
|
+
return env.CODEX_THREAD_ID ? 'codex' : 'claude';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A PostToolUse call's outcome. Claude Code never sends a failed command to PostToolUse (seen 2026-10-18), so there its silence means success. */
|
|
90
|
+
function runOutcome(payload, env = process.env) {
|
|
91
|
+
const said = outcome(payload.tool_response);
|
|
92
|
+
return said !== null ? said : clientOf(payload, env) === 'claude' ? true : null;
|
|
51
93
|
}
|
|
52
94
|
|
|
53
95
|
/** The journal line for a hook event, or null when the event carries nothing worth keeping. */
|
|
@@ -61,7 +103,7 @@ function entryFor(kind, payload, session, now = Date.now()) {
|
|
|
61
103
|
if (kind === 'tool') {
|
|
62
104
|
const heads = commandHeads(payload.tool_input?.command ?? '');
|
|
63
105
|
if (!heads.length) return null;
|
|
64
|
-
return { ...base, k: 'shell', heads, ok:
|
|
106
|
+
return { ...base, k: 'shell', heads, ok: runOutcome(payload) };
|
|
65
107
|
}
|
|
66
108
|
return kind === 'turn' ? { ...base, k: 'turn' } : null;
|
|
67
109
|
}
|
|
@@ -157,4 +199,4 @@ function brief(summary) {
|
|
|
157
199
|
return `; the journal holds ${r.prompts.length} prompts and ${r.corrections.length} corrections since the last knowledge turn and ${repeats} repeats over ${summary.days} days: read them with \`vibe knowledge journal\` and work from them rather than from memory`;
|
|
158
200
|
}
|
|
159
201
|
|
|
160
|
-
module.exports = { mask, commandHeads, outcome, entryFor, read, append, summarize, brief, fileName };
|
|
202
|
+
module.exports = { mask, segments, commandHeads, outcome, runOutcome, entryFor, read, append, summarize, brief, fileName };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The wiki's structure, checked and kept without a model: broken links, orphan pages, long pages, unknown types and
|
|
2
|
+
// pages sharing a title. `fixWiki` settles what needs no judgement (an orphan joins the index as a marked line, a
|
|
3
|
+
// marked line whose page is gone is removed, a project page log.md links to is created); the knowledge turn names
|
|
4
|
+
// the rest. Shared by `vibe knowledge lint` and the session hook, which runs it within the client's Stop time limit.
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { readNames, projectPage, writeIfChanged } = require('./knowledge-projects.cjs');
|
|
8
|
+
|
|
9
|
+
const MAX_PAGES = 2000;
|
|
10
|
+
const LONG_LINES = 200;
|
|
11
|
+
/** Pages that are entry points or append-only, so nothing has to link to them. */
|
|
12
|
+
const ROOTS = new Set(['index', 'log']);
|
|
13
|
+
/** What a page is, so the model knows whether to follow it, reuse it, avoid it or just know it; the wiki guide names them. */
|
|
14
|
+
const TYPES = ['decision', 'method', 'problem', 'fact', 'project', 'research', 'skill', 'proposal', 'source'];
|
|
15
|
+
/** Ends an index line vibe wrote, so vibe removes only its own lines; Obsidian's reading view hides it. */
|
|
16
|
+
const AUTO = '<!-- vibe:auto -->';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The wiki's pages, and the pages of each project's shared knowledge it shows through a folder link
|
|
20
|
+
* (`projects/<name>/`). Those are kept in the project, so they resolve links and are reported as unlinked, not orphans.
|
|
21
|
+
*/
|
|
22
|
+
function wikiPages(dir) {
|
|
23
|
+
const files = [];
|
|
24
|
+
const shared = new Set();
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
let partial = false;
|
|
27
|
+
const walk = (current, linked) => {
|
|
28
|
+
if (seen.has(fs.realpathSync(current))) return;
|
|
29
|
+
seen.add(fs.realpathSync(current));
|
|
30
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
31
|
+
if (entry.name.startsWith('.')) continue;
|
|
32
|
+
if (files.length >= MAX_PAGES) { partial = true; return; }
|
|
33
|
+
const file = path.join(current, entry.name);
|
|
34
|
+
if (entry.isDirectory()) walk(file, linked);
|
|
35
|
+
else if (entry.isSymbolicLink() && fs.statSync(file, { throwIfNoEntry: false })?.isDirectory()) walk(file, true);
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) { files.push(file); if (linked) shared.add(file); }
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (fs.existsSync(dir)) walk(dir, false);
|
|
40
|
+
return { files: files.sort(), shared, partial };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `[[target]]`, `[[target|label]]` and `[[target#heading]]` give `target`; links inside code are still counted. */
|
|
44
|
+
function wikiLinks(text) {
|
|
45
|
+
return [...text.matchAll(/\[\[([^\]|#\n]+)(?:[#|][^\]\n]*)?\]\]/g)].map(m => m[1].trim()).filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The `title` property, else the first `# heading`, else the file name. */
|
|
49
|
+
function pageTitle(text, file) {
|
|
50
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
51
|
+
const stated = front && /^title:\s*["']?(.+?)["']?\s*$/m.exec(front[1])?.[1];
|
|
52
|
+
const heading = /^#\s+(.+?)\s*$/m.exec(front ? text.slice(front[0].length) : text)?.[1];
|
|
53
|
+
return stated || heading || path.basename(file, '.md');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Each page's name as a link target, and every name a link may use for it (path from the root or file name alone). */
|
|
57
|
+
function names(dir, files) {
|
|
58
|
+
const name = file => path.relative(dir, file).split(path.sep).join('/').replace(/\.md$/, '');
|
|
59
|
+
const known = new Map();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
known.set(name(file).toLowerCase(), name(file));
|
|
62
|
+
known.set(path.basename(file, '.md').toLowerCase(), name(file));
|
|
63
|
+
}
|
|
64
|
+
return { name, resolve: target => known.get(target.replace(/\.md$/, '').toLowerCase()) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Broken links, pages nothing links to, pages too long to read in one pass, unknown types and pages sharing a
|
|
69
|
+
* title. A link resolves the way Obsidian resolves it; a link from log.md finds a page but does not make it reachable.
|
|
70
|
+
*/
|
|
71
|
+
function lintWiki(dir) {
|
|
72
|
+
const { files, shared, partial } = wikiPages(dir);
|
|
73
|
+
const { name, resolve } = names(dir, files);
|
|
74
|
+
const broken = [], long = [], badType = [];
|
|
75
|
+
const linked = new Set();
|
|
76
|
+
const titles = new Map();
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
79
|
+
const lines = text.split('\n').length;
|
|
80
|
+
const type = /^---\r?\n(?:[^\n]*\n)*?type:\s*["']?([^"'\n]+?)["']?\s*\n(?:[^\n]*\n)*?---/.exec(text)?.[1];
|
|
81
|
+
// Source material (a transcript, a web clipping, a video analysis) is long by nature and never split.
|
|
82
|
+
if (lines > LONG_LINES && type?.toLowerCase() !== 'source') long.push({ page: name(file), lines });
|
|
83
|
+
if (type !== undefined && !TYPES.includes(type.toLowerCase())) badType.push({ page: name(file), type });
|
|
84
|
+
if (!shared.has(file) && !ROOTS.has(name(file))) {
|
|
85
|
+
const title = pageTitle(text, file).toLowerCase();
|
|
86
|
+
titles.set(title, [...(titles.get(title) ?? []), name(file)]);
|
|
87
|
+
}
|
|
88
|
+
for (const target of wikiLinks(text)) {
|
|
89
|
+
const page = resolve(target);
|
|
90
|
+
if (page === undefined) broken.push({ page: name(file), target });
|
|
91
|
+
else if (page !== name(file) && name(file) !== 'log') linked.add(page);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const orphans = files.filter(file => !shared.has(file)).map(name).filter(page => !linked.has(page) && !ROOTS.has(page));
|
|
95
|
+
const unlinked = files.filter(file => shared.has(file)).map(name).filter(page => !linked.has(page));
|
|
96
|
+
const duplicates = [...titles].filter(([, pages]) => pages.length > 1).map(([, pages]) => pages);
|
|
97
|
+
return { dir, pages: files.length, broken, orphans, unlinked, long, badType, duplicates, partial };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Settle what needs no judgement: create each project page log.md links to that is missing, add every orphan to
|
|
102
|
+
* index.md as a marked line, and remove marked lines whose page is gone. A person's line is never touched; its
|
|
103
|
+
* broken link stays in the lint. Returns what changed and the lint after it.
|
|
104
|
+
*/
|
|
105
|
+
function fixWiki(dir, home) {
|
|
106
|
+
const created = [];
|
|
107
|
+
const log = path.join(dir, 'log.md');
|
|
108
|
+
if (fs.existsSync(log)) {
|
|
109
|
+
const { files } = wikiPages(dir);
|
|
110
|
+
const { resolve } = names(dir, files);
|
|
111
|
+
const roots = readNames(home);
|
|
112
|
+
for (const target of new Set(wikiLinks(fs.readFileSync(log, 'utf8')))) {
|
|
113
|
+
const project = /^projects\/([^/]+)$/.exec(target.replace(/\.md$/, ''))?.[1];
|
|
114
|
+
if (project && resolve(target) === undefined && projectPage(dir, project, roots[project])) created.push(`projects/${project}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const { files, shared } = wikiPages(dir);
|
|
118
|
+
const { name, resolve } = names(dir, files);
|
|
119
|
+
const index = path.join(dir, 'index.md');
|
|
120
|
+
const text = fs.existsSync(index) ? fs.readFileSync(index, 'utf8') : '# Index\n';
|
|
121
|
+
const removed = [];
|
|
122
|
+
const kept = text.split('\n').filter(line => {
|
|
123
|
+
const target = line.includes(AUTO) ? wikiLinks(line)[0] : undefined;
|
|
124
|
+
if (target === undefined || resolve(target) !== undefined) return true;
|
|
125
|
+
removed.push(target);
|
|
126
|
+
return false;
|
|
127
|
+
});
|
|
128
|
+
const added = lintWiki(dir).orphans.map(page => {
|
|
129
|
+
const file = files.find(f => !shared.has(f) && name(f) === page);
|
|
130
|
+
return { page, title: pageTitle(fs.readFileSync(file, 'utf8'), file).replace(/\s+/g, ' ') };
|
|
131
|
+
});
|
|
132
|
+
if (added.length || removed.length) {
|
|
133
|
+
const body = kept.join('\n').trimEnd();
|
|
134
|
+
writeIfChanged(index, `${body}\n${added.map(a => `- [[${a.page}]] — ${a.title} ${AUTO}\n`).join('')}`);
|
|
135
|
+
}
|
|
136
|
+
return { created, added, removed, lint: lintWiki(dir) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Pages changed since `since` (index and log aside) and the pages they link to, for a review turn. */
|
|
140
|
+
function reviewScope(dir, since) {
|
|
141
|
+
const { files } = wikiPages(dir);
|
|
142
|
+
const { name, resolve } = names(dir, files);
|
|
143
|
+
const changed = files.filter(file => !ROOTS.has(name(file)) && fs.statSync(file).mtimeMs > since);
|
|
144
|
+
const linked = new Set();
|
|
145
|
+
for (const file of changed) for (const target of wikiLinks(fs.readFileSync(file, 'utf8'))) linked.add(resolve(target));
|
|
146
|
+
const pages = changed.map(name);
|
|
147
|
+
return { changed: pages, linked: [...linked].filter(page => page && !pages.includes(page) && !ROOTS.has(page)).sort() };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { TYPES, AUTO, wikiPages, wikiLinks, pageTitle, lintWiki, fixWiki, reviewScope };
|
|
@@ -110,4 +110,15 @@ function indexProject(root, wiki, name) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Create the project's page when it is missing, so a log line's `[[projects/<name>]]` always resolves. Returns
|
|
115
|
+
* whether it created one; an existing page is left as it is.
|
|
116
|
+
*/
|
|
117
|
+
function projectPage(wiki, name, root) {
|
|
118
|
+
const page = path.join(wiki, 'projects', `${name}.md`);
|
|
119
|
+
if (fs.existsSync(page)) return false;
|
|
120
|
+
writeIfChanged(page, `---\ntype: project\n---\n# ${name}\n${typeof root === 'string' ? `\nFolder: \`${root}\`\n` : ''}`);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { readNames, projectName, linkProject, indexProject, projectPage, writeIfChanged };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// What a knowledge turn adds about the wiki itself: the lint fix runs first, and the reason names only what it could
|
|
2
|
+
// not settle; after enough changed pages or days, the turn also reviews those pages and the pages they link.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { fixWiki, reviewScope } = require('./knowledge-lint.cjs');
|
|
6
|
+
const { writeIfChanged } = require('./knowledge-projects.cjs');
|
|
7
|
+
|
|
8
|
+
const DAY = 86400000;
|
|
9
|
+
const SHOWN = 3;
|
|
10
|
+
const LISTED = 20;
|
|
11
|
+
|
|
12
|
+
const few = (items, show) => `${items.slice(0, SHOWN).map(show).join(', ')}${items.length > SHOWN ? ` and ${items.length - SHOWN} more` : ''}`;
|
|
13
|
+
|
|
14
|
+
/** Run the fix, then a clause naming what needs judgement, or '' when nothing is left. */
|
|
15
|
+
function tidy(dir, home) {
|
|
16
|
+
try {
|
|
17
|
+
const { lint } = fixWiki(dir, home);
|
|
18
|
+
const left = [
|
|
19
|
+
lint.broken.length ? `broken links ${few(lint.broken, b => `${b.page} → [[${b.target}]]`)}` : '',
|
|
20
|
+
lint.long.length ? `long pages ${few(lint.long, l => `${l.page} (${l.lines} lines)`)}` : '',
|
|
21
|
+
lint.badType.length ? `unknown types ${few(lint.badType, b => `${b.page} "${b.type}"`)}` : '',
|
|
22
|
+
lint.duplicates.length ? `pages sharing a title ${few(lint.duplicates, pages => pages.join(' = '))}` : '',
|
|
23
|
+
].filter(Boolean);
|
|
24
|
+
return left.length ? ` The wiki lint fixed what needs no judgement; left for you: ${left.join('; ')}. Settle them as the guide's "Keep the connections" says (\`vibe knowledge lint\` lists all).` : '';
|
|
25
|
+
} catch { return ''; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const stateFile = home => path.join(home, '.config', 'vibe', 'knowledge-review.json');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A review clause when `pages` pages changed since the last review, or one did and `days` days passed; '' otherwise.
|
|
32
|
+
* The first call only sets the starting point, so turning this on never reviews the whole wiki at once.
|
|
33
|
+
*/
|
|
34
|
+
function review(dir, home, pages, days, now = Date.now()) {
|
|
35
|
+
if (!pages) return '';
|
|
36
|
+
try {
|
|
37
|
+
let since = null;
|
|
38
|
+
try { since = JSON.parse(fs.readFileSync(stateFile(home), 'utf8')).at; } catch { /* first review */ }
|
|
39
|
+
if (!Number.isFinite(since)) { writeIfChanged(stateFile(home), JSON.stringify({ at: now })); return ''; }
|
|
40
|
+
const scope = reviewScope(dir, since);
|
|
41
|
+
const due = scope.changed.length >= pages || (scope.changed.length > 0 && days > 0 && now - since >= days * DAY);
|
|
42
|
+
if (!due) return '';
|
|
43
|
+
writeIfChanged(stateFile(home), JSON.stringify({ at: now }));
|
|
44
|
+
const list = items => `${items.slice(0, LISTED).map(p => `[[${p}]]`).join(', ')}${items.length > LISTED ? ` and ${items.length - LISTED} more` : ''}`;
|
|
45
|
+
return ` This is also a review turn (${scope.changed.length} pages changed since ${new Date(since).toISOString().slice(0, 10)}): read ${list(scope.changed)}${scope.linked.length ? ` and the pages they link, ${list(scope.linked)},` : ''} for claims that contradict each other, dated claims likely out of date and decisions a later one replaced, and correct them as the guide's "Review" says.`;
|
|
46
|
+
} catch { return ''; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { tidy, review, stateFile };
|
package/hooks/notify.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* pre PreToolUse(Bash): an irreversible command with no recent authorize record is blocked (exit 2) under the
|
|
7
7
|
* strict and irreversible token policies, and only warned about under off.
|
|
8
8
|
* Partial Bash reads of protected files are blocked; other source/document slices are warned.
|
|
9
|
+
* A test command that already passed on unchanged code is blocked or warned about (retest.cjs).
|
|
9
10
|
* PreToolUse(Read): when the file is over READ_ADVISE_LINES, tells the model that `vibe read <file> --ask`
|
|
10
11
|
* lets a low-reasoning model read it — advice only, the read goes ahead.
|
|
11
12
|
*
|
|
@@ -17,6 +18,7 @@ import os from 'node:os';
|
|
|
17
18
|
import path from 'node:path';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
19
20
|
import knowledgeAuto from './knowledge-auto.cjs';
|
|
21
|
+
import retest from './retest.cjs';
|
|
20
22
|
import sessionRuntime from './session-state.cjs';
|
|
21
23
|
import sessionFiles from './session-files.cjs';
|
|
22
24
|
import sliceRuntime from './slice-guard.cjs';
|
|
@@ -160,13 +162,21 @@ if (mode === 'pre') {
|
|
|
160
162
|
if (fs.existsSync(path.join(root, '.vibe'))) adviseRead(payload);
|
|
161
163
|
process.exit(0);
|
|
162
164
|
}
|
|
163
|
-
const
|
|
165
|
+
const raw = payload.tool_input && payload.tool_input.command;
|
|
166
|
+
const command = Array.isArray(raw) ? raw.join(' ') : String(raw || '');
|
|
164
167
|
const cwd = payload.tool_input?.cwd ?? payload.tool_input?.workdir ?? payload.cwd ?? process.cwd();
|
|
165
|
-
|
|
168
|
+
// One additionalContext output per call: warnings collect here and leave with the exit.
|
|
169
|
+
const notes = [];
|
|
170
|
+
const leave = (code) => { if (notes.length) emitContext(notes.join('\n')); process.exit(code); };
|
|
171
|
+
let gate = null;
|
|
172
|
+
try { gate = retest.before(payload, process.env.VIBE_HOME_DIR || os.homedir()); } catch { /* the command runs */ }
|
|
173
|
+
if (gate?.decision === 'block') { process.stderr.write(`${gate.message}\n`); process.exit(2); }
|
|
174
|
+
if (gate?.decision === 'warn') notes.push(gate.message);
|
|
175
|
+
if (personal && tokenPolicy() === 'off') leave(0);
|
|
166
176
|
const slice = personal ? { decision: 'allow' } : sliceRuntime.sliceGuard(root, command, sliceClient(), cwd);
|
|
167
177
|
if (slice.decision === 'block') { process.stderr.write(`${slice.message}\n`); process.exit(2); }
|
|
168
|
-
if (slice.decision === 'warn')
|
|
169
|
-
if (!fs.existsSync(path.join(root, '.vibe')))
|
|
178
|
+
if (slice.decision === 'warn') notes.push(slice.message);
|
|
179
|
+
if (!fs.existsSync(path.join(root, '.vibe'))) leave(0);
|
|
170
180
|
// Every segment is judged — `echo x && git push`, `ls; rm -rf build`, `cat x | git push` carry the action in a later one
|
|
171
181
|
// Vibe gates its own execution; its approval and inbox commands must remain reachable.
|
|
172
182
|
const segments = command.split(/&&|\|\||;|\||\n/).map((s) => s.trim()).filter((s) => s && !READS_ONLY.test(s) && !/^vibe(?:\s|$)/.test(s));
|
|
@@ -175,10 +185,11 @@ if (mode === 'pre') {
|
|
|
175
185
|
if (segments.some((s) => re.test(s)) && !recentAuthorize(action)) {
|
|
176
186
|
const blocking = tokenPolicy() !== 'off';
|
|
177
187
|
process.stderr.write(`[vibe] "${action}" is irreversible and no authorize record exists in the last 10 minutes — ${blocking ? 'blocked: ' : ''}get a human token with \`vibe ask --needs authorize:${action}\` and run \`vibe authorize\` first, as its own command: the gate reads the ledger before this command runs, so an authorize chained in front of the action is not seen\n`);
|
|
178
|
-
process.exit(
|
|
188
|
+
if (blocking) process.exit(2);
|
|
189
|
+
leave(0);
|
|
179
190
|
}
|
|
180
191
|
}
|
|
181
|
-
|
|
192
|
+
leave(0);
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
// Inside the npm package dist/cli.js sits next to us; inside a plugin tree it does not, so fall back to `vibe` on PATH.
|