wendkeep 0.58.3 → 0.60.0
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/CHANGELOG.md +93 -0
- package/README.en.md +45 -3
- package/README.md +45 -3
- package/bin/wendkeep.mjs +54 -6
- package/docs/en/commands/changes-and-verification.md +9 -3
- package/docs/en/commands/getting-started.md +7 -3
- package/docs/en/commands/memory.md +20 -2
- package/docs/en/commands/operating-profiles.md +173 -0
- package/docs/en/commands/sessions-and-import.md +8 -4
- package/docs/en/commands/verify.md +12 -6
- package/docs/pt-BR/commands/changes-and-verification.md +9 -4
- package/docs/pt-BR/commands/getting-started.md +7 -3
- package/docs/pt-BR/commands/memory.md +18 -2
- package/docs/pt-BR/commands/operating-profiles.md +171 -0
- package/docs/pt-BR/commands/sessions-and-import.md +7 -3
- package/docs/pt-BR/commands/verify.md +11 -5
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-inject.mjs +83 -26
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-context.mjs +24 -10
- package/hooks/change-core.mjs +174 -37
- package/hooks/change-guard.mjs +115 -16
- package/hooks/change-nag.mjs +20 -5
- package/hooks/change-warn.mjs +27 -9
- package/hooks/decision-capture.mjs +1 -1
- package/hooks/derived-sections.mjs +1 -1
- package/hooks/flow-core.mjs +891 -0
- package/hooks/flow-protected-policy.mjs +218 -0
- package/hooks/frontmatter-repair.mjs +3 -1
- package/hooks/git-snapshot.mjs +722 -0
- package/hooks/import-sessions.mjs +10 -5
- package/hooks/memory-mode.mjs +63 -13
- package/hooks/memory-store.mjs +309 -69
- package/hooks/obsidian-common.mjs +39 -55
- package/hooks/operating-profile-runtime.mjs +157 -0
- package/hooks/plan-capture.mjs +14 -3
- package/hooks/sensors-core.mjs +15 -3
- package/hooks/session-backfill.mjs +7 -2
- package/hooks/session-ensure.mjs +6 -4
- package/hooks/session-iteration.mjs +65 -0
- package/hooks/session-memory-lifecycle.mjs +10 -5
- package/hooks/session-note-io.mjs +130 -15
- package/hooks/session-observability.mjs +4 -2
- package/hooks/session-stop.mjs +65 -19
- package/hooks/spec-core.mjs +91 -12
- package/hooks/subagent-stop.mjs +4 -1
- package/hooks/subagent-usage.mjs +2 -2
- package/hooks/task-log.mjs +3 -1
- package/hooks/token-usage.mjs +1 -1
- package/hooks/vault-health.mjs +183 -37
- package/hooks/vault-path-safety.mjs +2 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +10 -3
- package/packages/cli/package.json +5 -0
- package/packages/harness/package.json +5 -0
- package/packages/integrations/package.json +5 -0
- package/packages/mcp/package.json +5 -0
- package/packages/pi/package.json +5 -0
- package/packages/vault/package.json +6 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/project-vault.mjs +327 -0
- package/packages/vault/src/vault-path-safety.mjs +558 -0
- package/src/change.mjs +2 -1
- package/src/flow.mjs +232 -0
- package/src/init.mjs +26 -3
- package/src/memory.mjs +785 -35
- package/src/operating-profile.mjs +133 -0
- package/src/profile.mjs +224 -0
- package/src/project-vault.mjs +2 -221
- package/src/rebuild-costs.mjs +11 -4
- package/src/skills-seed.mjs +38 -16
- package/src/sync-defs.mjs +16 -7
- package/src/sync.mjs +9 -1
- package/src/taxonomy.mjs +8 -0
- package/src/validate-memory.mjs +21 -8
- package/src/verify.mjs +12 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export const OPERATING_PROFILES = Object.freeze([
|
|
2
|
+
'OFF',
|
|
3
|
+
'FLOW',
|
|
4
|
+
'GUIDE',
|
|
5
|
+
'GOVERN',
|
|
6
|
+
'ASSURE',
|
|
7
|
+
]);
|
|
8
|
+
export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
|
|
9
|
+
|
|
10
|
+
const PROFILE_SET = new Set(OPERATING_PROFILES);
|
|
11
|
+
|
|
12
|
+
function policy(profile, route, options) {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
profile,
|
|
15
|
+
route: Object.freeze(route),
|
|
16
|
+
keepCore: true,
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OPERATING_PROFILE_POLICIES = Object.freeze({
|
|
22
|
+
OFF: policy('OFF', ['LLM'], {
|
|
23
|
+
harness: false,
|
|
24
|
+
contract: 'native',
|
|
25
|
+
requiresChange: false,
|
|
26
|
+
requiresReview: false,
|
|
27
|
+
requiresConfirmation: false,
|
|
28
|
+
}),
|
|
29
|
+
FLOW: policy('FLOW', ['E', 'V'], {
|
|
30
|
+
harness: true,
|
|
31
|
+
contract: 'flow',
|
|
32
|
+
requiresChange: false,
|
|
33
|
+
requiresReview: false,
|
|
34
|
+
requiresConfirmation: false,
|
|
35
|
+
}),
|
|
36
|
+
GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
|
|
37
|
+
harness: true,
|
|
38
|
+
contract: 'simple-change',
|
|
39
|
+
requiresChange: true,
|
|
40
|
+
requiresReview: false,
|
|
41
|
+
requiresConfirmation: false,
|
|
42
|
+
}),
|
|
43
|
+
GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
|
|
44
|
+
harness: true,
|
|
45
|
+
contract: 'change',
|
|
46
|
+
requiresChange: true,
|
|
47
|
+
requiresReview: true,
|
|
48
|
+
requiresConfirmation: false,
|
|
49
|
+
}),
|
|
50
|
+
ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
|
|
51
|
+
harness: true,
|
|
52
|
+
contract: 'change',
|
|
53
|
+
requiresChange: true,
|
|
54
|
+
requiresReview: true,
|
|
55
|
+
requiresConfirmation: true,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function invalidProfileError(value) {
|
|
60
|
+
const rendered = typeof value === 'string' ? `"${value}"` : String(value);
|
|
61
|
+
const error = new Error(
|
|
62
|
+
`Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
|
|
63
|
+
);
|
|
64
|
+
error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalProfile(value) {
|
|
69
|
+
if (typeof value !== 'string') return '';
|
|
70
|
+
return value.trim().toUpperCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeOperatingProfile(value, { strict = false } = {}) {
|
|
74
|
+
const normalized = canonicalProfile(value);
|
|
75
|
+
if (PROFILE_SET.has(normalized)) return normalized;
|
|
76
|
+
if (strict) throw invalidProfileError(value);
|
|
77
|
+
return DEFAULT_OPERATING_PROFILE;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveOperatingProfile(config = {}) {
|
|
81
|
+
const harness = config && typeof config === 'object' && !Array.isArray(config)
|
|
82
|
+
&& config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
|
|
83
|
+
? config.harness
|
|
84
|
+
: null;
|
|
85
|
+
const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
|
|
86
|
+
if (!configured) {
|
|
87
|
+
return {
|
|
88
|
+
profile: DEFAULT_OPERATING_PROFILE,
|
|
89
|
+
source: 'default',
|
|
90
|
+
valid: true,
|
|
91
|
+
configured: false,
|
|
92
|
+
raw: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const raw = harness.profile;
|
|
97
|
+
const normalized = canonicalProfile(raw);
|
|
98
|
+
if (PROFILE_SET.has(normalized)) {
|
|
99
|
+
return {
|
|
100
|
+
profile: normalized,
|
|
101
|
+
source: 'project-binding',
|
|
102
|
+
valid: true,
|
|
103
|
+
configured: true,
|
|
104
|
+
raw,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
profile: DEFAULT_OPERATING_PROFILE,
|
|
109
|
+
source: 'default-invalid',
|
|
110
|
+
valid: false,
|
|
111
|
+
configured: true,
|
|
112
|
+
raw,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function operatingProfilePolicy(value) {
|
|
117
|
+
return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setOperatingProfile(config = {}, value) {
|
|
121
|
+
const profile = normalizeOperatingProfile(value, { strict: true });
|
|
122
|
+
const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
|
123
|
+
const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
|
|
124
|
+
? base.harness
|
|
125
|
+
: {};
|
|
126
|
+
return {
|
|
127
|
+
...base,
|
|
128
|
+
harness: {
|
|
129
|
+
...harness,
|
|
130
|
+
profile,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/profile.mjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Public operating-profile CLI. Keep project binding and session override mutations atomic,
|
|
2
|
+
// while profile policy itself remains pure in operating-profile.mjs.
|
|
3
|
+
import { mutateSessionRegistry, readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_OPERATING_PROFILE,
|
|
6
|
+
normalizeOperatingProfile,
|
|
7
|
+
resolveOperatingProfile,
|
|
8
|
+
setOperatingProfile,
|
|
9
|
+
} from './operating-profile.mjs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { findProjectBinding, resolveProjectVault, updateProjectBinding } from './project-vault.mjs';
|
|
12
|
+
|
|
13
|
+
export const PROFILE_HELP = `wendkeep profile <subcommand>
|
|
14
|
+
|
|
15
|
+
status [--session <id>]
|
|
16
|
+
use <OFF|FLOW|GUIDE|GOVERN|ASSURE> [--session <id>]
|
|
17
|
+
|
|
18
|
+
Common options: --project <path> --vault <path> --session <id> --json
|
|
19
|
+
The Keep Core (Vault, session, and memory) remains active under every profile.
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
|
|
23
|
+
const FLAG_OPTIONS = new Set(['--json']);
|
|
24
|
+
|
|
25
|
+
function optionValue(argv, name) {
|
|
26
|
+
const index = argv.indexOf(name);
|
|
27
|
+
if (index >= 0) return argv[index + 1] || '';
|
|
28
|
+
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandArgs(argv) {
|
|
32
|
+
const values = [];
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const value = argv[index];
|
|
35
|
+
if (['--project', '--vault', '--session'].includes(value)) { index += 1; continue; }
|
|
36
|
+
if (value.startsWith('--project=') || value.startsWith('--vault=') || value.startsWith('--session=')) continue;
|
|
37
|
+
if (value === '--json') continue;
|
|
38
|
+
values.push(value);
|
|
39
|
+
}
|
|
40
|
+
return values;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateArgv(argv) {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
46
|
+
const value = argv[index];
|
|
47
|
+
if (FLAG_OPTIONS.has(value)) {
|
|
48
|
+
if (seen.has(value)) throw new Error(`opção duplicada: ${value}`);
|
|
49
|
+
seen.add(value);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (VALUE_OPTIONS.has(value)) {
|
|
53
|
+
if (seen.has(value)) throw new Error(`opção duplicada: ${value}`);
|
|
54
|
+
seen.add(value);
|
|
55
|
+
const next = argv[index + 1];
|
|
56
|
+
if (!next || next.startsWith('--')) throw new Error(`${value} requer um valor`);
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (value.startsWith('--')) {
|
|
61
|
+
const name = value.split('=', 1)[0];
|
|
62
|
+
if (!VALUE_OPTIONS.has(name)) throw new Error(`opção desconhecida: ${name}`);
|
|
63
|
+
if (seen.has(name)) throw new Error(`opção duplicada: ${name}`);
|
|
64
|
+
seen.add(name);
|
|
65
|
+
const inlineValue = value.slice(name.length + 1);
|
|
66
|
+
if (!inlineValue || inlineValue.startsWith('--')) throw new Error(`${name} requer um valor`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalPath(value) {
|
|
72
|
+
const path = resolve(value).replaceAll('\\', '/');
|
|
73
|
+
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function output(payload, json) {
|
|
77
|
+
if (json) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
78
|
+
else {
|
|
79
|
+
const scope = payload.scope === 'session' ? `session ${payload.session_id}` : 'project';
|
|
80
|
+
process.stdout.write(`${payload.profile} (${scope}; ${payload.source})\n`);
|
|
81
|
+
}
|
|
82
|
+
if (payload.binding_error) {
|
|
83
|
+
const code = payload.binding_error.code || 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
84
|
+
process.stderr.write(`wendkeep profile: ${code}: ${payload.binding_error.message || 'binding WendKeep inválido'}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fail(message) {
|
|
89
|
+
process.stderr.write(`wendkeep profile: ${message}\n`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function context(argv) {
|
|
94
|
+
const explicitVault = optionValue(argv, '--vault');
|
|
95
|
+
const startDir = optionValue(argv, '--project') || process.cwd();
|
|
96
|
+
const resolved = resolveProjectVault({ startDir, explicitVault });
|
|
97
|
+
let binding = null;
|
|
98
|
+
try { binding = findProjectBinding(startDir); }
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!explicitVault) throw error;
|
|
101
|
+
}
|
|
102
|
+
const matchingBinding = binding && canonicalPath(binding.base) === canonicalPath(resolved.base) ? binding : null;
|
|
103
|
+
const projectConfig = resolved.config || matchingBinding?.config || {};
|
|
104
|
+
return {
|
|
105
|
+
resolved: {
|
|
106
|
+
...resolved,
|
|
107
|
+
projectRoot: matchingBinding?.projectRoot || (resolved.config ? resolved.projectRoot : null),
|
|
108
|
+
},
|
|
109
|
+
projectConfig,
|
|
110
|
+
vaultBase: resolved.base,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function setSessionOperatingProfile(vaultBase, sessionId, profile, { now } = {}) {
|
|
115
|
+
const selected = normalizeOperatingProfile(profile, { strict: true });
|
|
116
|
+
const updatedAt = now || new Date().toISOString();
|
|
117
|
+
return mutateSessionRegistry(vaultBase, (registry) => {
|
|
118
|
+
const sessions = registry.sessions || (registry.sessions = {});
|
|
119
|
+
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
120
|
+
const current = sessions[sessionId];
|
|
121
|
+
sessions[sessionId] = {
|
|
122
|
+
...current,
|
|
123
|
+
operating_profile: selected,
|
|
124
|
+
operating_profile_source: 'explicit-cli',
|
|
125
|
+
operating_profile_updated_at: updatedAt,
|
|
126
|
+
updated_at: updatedAt,
|
|
127
|
+
};
|
|
128
|
+
return sessions[sessionId];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sessionProfile(vaultBase, sessionId, projectResolved) {
|
|
133
|
+
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
134
|
+
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
135
|
+
const entry = sessions[sessionId];
|
|
136
|
+
if (Object.hasOwn(entry, 'operating_profile')) {
|
|
137
|
+
try {
|
|
138
|
+
return {
|
|
139
|
+
profile: normalizeOperatingProfile(entry.operating_profile, { strict: true }),
|
|
140
|
+
source: 'session-registry',
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return {
|
|
144
|
+
profile: DEFAULT_OPERATING_PROFILE,
|
|
145
|
+
source: 'session-override-invalid',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { profile: projectResolved.profile, source: projectResolved.source };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function runProfile(argv = []) {
|
|
153
|
+
try { validateArgv(argv); }
|
|
154
|
+
catch (error) { return fail(error.message); }
|
|
155
|
+
const args = commandArgs(argv);
|
|
156
|
+
const sub = args[0] || 'status';
|
|
157
|
+
const json = argv.includes('--json');
|
|
158
|
+
const sessionId = optionValue(argv, '--session') || '';
|
|
159
|
+
|
|
160
|
+
let state;
|
|
161
|
+
try { state = context(argv); }
|
|
162
|
+
catch (error) { return fail(error.message); }
|
|
163
|
+
|
|
164
|
+
const projectResolved = resolveOperatingProfile(state.projectConfig);
|
|
165
|
+
if (sub === 'status' || sub === 'show') {
|
|
166
|
+
if (args.length > 1) return fail(`${sub} não aceita argumentos posicionais adicionais`);
|
|
167
|
+
try {
|
|
168
|
+
const effective = sessionId
|
|
169
|
+
? sessionProfile(state.vaultBase, sessionId, projectResolved)
|
|
170
|
+
: { profile: projectResolved.profile, source: projectResolved.source };
|
|
171
|
+
output({
|
|
172
|
+
profile: effective.profile,
|
|
173
|
+
source: effective.source,
|
|
174
|
+
scope: sessionId ? 'session' : 'project',
|
|
175
|
+
session_id: sessionId || null,
|
|
176
|
+
...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
|
|
177
|
+
}, json);
|
|
178
|
+
return 0;
|
|
179
|
+
} catch (error) { return fail(error.message); }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (sub !== 'use' && sub !== 'set') return fail('use status | use <OFF|FLOW|GUIDE|GOVERN|ASSURE>');
|
|
183
|
+
if (args.length !== 2) return fail(`${sub} requer exatamente um perfil`);
|
|
184
|
+
let profile;
|
|
185
|
+
try { profile = normalizeOperatingProfile(args[1], { strict: true }); }
|
|
186
|
+
catch { return fail('perfil inválido; use OFF, FLOW, GUIDE, GOVERN ou ASSURE'); }
|
|
187
|
+
|
|
188
|
+
if (sessionId) {
|
|
189
|
+
try { setSessionOperatingProfile(state.vaultBase, sessionId, profile); }
|
|
190
|
+
catch (error) { return fail(error.message); }
|
|
191
|
+
output({
|
|
192
|
+
profile,
|
|
193
|
+
source: 'session-registry',
|
|
194
|
+
scope: 'session',
|
|
195
|
+
session_id: sessionId,
|
|
196
|
+
...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
|
|
197
|
+
}, json);
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!state.resolved.projectRoot) return fail('binding de projeto necessário para alterar o perfil padrão');
|
|
202
|
+
try {
|
|
203
|
+
const updatedAt = new Date().toISOString();
|
|
204
|
+
updateProjectBinding(state.resolved.projectRoot, (current) => {
|
|
205
|
+
const next = setOperatingProfile(current, profile);
|
|
206
|
+
return {
|
|
207
|
+
...next,
|
|
208
|
+
harness: {
|
|
209
|
+
...next.harness,
|
|
210
|
+
profileSource: 'explicit-cli',
|
|
211
|
+
profileUpdatedAt: updatedAt,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
} catch (error) { return fail(error.message); }
|
|
216
|
+
output({
|
|
217
|
+
profile,
|
|
218
|
+
source: 'project-binding',
|
|
219
|
+
scope: 'project',
|
|
220
|
+
session_id: null,
|
|
221
|
+
...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
|
|
222
|
+
}, json);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
package/src/project-vault.mjs
CHANGED
|
@@ -1,221 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
|
-
statSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from 'node:fs';
|
|
10
|
-
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
11
|
-
|
|
12
|
-
export const PROJECT_CONFIG_FILE = '.wendkeep.json';
|
|
13
|
-
export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
|
|
14
|
-
export const PROJECT_CONFIG_SCHEMA = 1;
|
|
15
|
-
|
|
16
|
-
function json(path) {
|
|
17
|
-
try { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
18
|
-
catch (error) {
|
|
19
|
-
const wrapped = new Error(`Configuração WendKeep inválida em "${path}": ${error.message}`);
|
|
20
|
-
wrapped.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
21
|
-
throw wrapped;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function atomicJson(path, value) {
|
|
26
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
-
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
28
|
-
if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
|
|
29
|
-
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
30
|
-
writeFileSync(temp, content, 'utf8');
|
|
31
|
-
renameSync(temp, path);
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function startDirectory(value) {
|
|
36
|
-
const candidate = resolve(String(value || process.cwd()));
|
|
37
|
-
try { return statSync(candidate).isFile() ? dirname(candidate) : candidate; }
|
|
38
|
-
catch { return candidate; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function walkParents(start) {
|
|
42
|
-
const result = [];
|
|
43
|
-
let current = startDirectory(start);
|
|
44
|
-
const root = parse(current).root;
|
|
45
|
-
while (true) {
|
|
46
|
-
result.push(current);
|
|
47
|
-
if (current === root) break;
|
|
48
|
-
const parent = dirname(current);
|
|
49
|
-
if (parent === current) break;
|
|
50
|
-
current = parent;
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function inputStart(input = {}, fallback = '') {
|
|
56
|
-
return input.cwd
|
|
57
|
-
|| input.project_dir
|
|
58
|
-
|| input.projectDir
|
|
59
|
-
|| input.workspace?.cwd
|
|
60
|
-
|| process.env.CLAUDE_PROJECT_DIR
|
|
61
|
-
|| fallback
|
|
62
|
-
|| process.cwd();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function vaultFromConfig(projectRoot, config) {
|
|
66
|
-
if (!config || config.schemaVersion !== PROJECT_CONFIG_SCHEMA || !config.projectId || !config.vault) {
|
|
67
|
-
const error = new Error(
|
|
68
|
-
`Configuração incompleta em "${join(projectRoot, PROJECT_CONFIG_FILE)}". `
|
|
69
|
-
+ 'Rode `wendkeep init --project <path> --vault <path>`.',
|
|
70
|
-
);
|
|
71
|
-
error.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
return isAbsolute(config.vault) ? resolve(config.vault) : resolve(projectRoot, config.vault);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function readProjectBinding(projectRoot) {
|
|
78
|
-
const root = resolve(projectRoot);
|
|
79
|
-
const path = join(root, PROJECT_CONFIG_FILE);
|
|
80
|
-
if (!existsSync(path)) return null;
|
|
81
|
-
const config = json(path);
|
|
82
|
-
return { config, configPath: path, projectRoot: root, base: vaultFromConfig(root, config) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function findProjectBinding(start) {
|
|
86
|
-
for (const projectRoot of walkParents(start)) {
|
|
87
|
-
const found = readProjectBinding(projectRoot);
|
|
88
|
-
if (found) return found;
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function findLegacyProjectVault(start) {
|
|
94
|
-
for (const projectRoot of walkParents(start)) {
|
|
95
|
-
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
96
|
-
if (!existsSync(settingsPath)) continue;
|
|
97
|
-
try {
|
|
98
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
99
|
-
const raw = settings?.env?.OBSIDIAN_VAULT_PATH;
|
|
100
|
-
if (typeof raw === 'string' && raw.trim()) {
|
|
101
|
-
return {
|
|
102
|
-
base: isAbsolute(raw) ? resolve(raw) : resolve(projectRoot, raw),
|
|
103
|
-
projectRoot,
|
|
104
|
-
source: 'legacy-project-settings',
|
|
105
|
-
configPath: settingsPath,
|
|
106
|
-
projectId: '',
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
} catch { /* init/doctor explicam JSON inválido; descoberta segue procurando */ }
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function readVaultMarker(vaultPath) {
|
|
115
|
-
const markerPath = join(resolve(vaultPath), ...PROJECT_MARKER_REL.split('/'));
|
|
116
|
-
if (!existsSync(markerPath)) return null;
|
|
117
|
-
return { marker: json(markerPath), markerPath };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function validateMarker(result) {
|
|
121
|
-
const found = readVaultMarker(result.base);
|
|
122
|
-
if (!found) {
|
|
123
|
-
const error = new Error(
|
|
124
|
-
`O vault "${result.base}" ainda não possui ${PROJECT_MARKER_REL}. `
|
|
125
|
-
+ `Rode \`wendkeep init --project "${result.projectRoot}" --vault "${result.base}" --yes\`.`,
|
|
126
|
-
);
|
|
127
|
-
error.code = 'WENDKEEP_VAULT_MARKER_MISSING';
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
if (found.marker?.projectId !== result.projectId) {
|
|
131
|
-
const error = new Error(
|
|
132
|
-
`Vault de outro projeto: configuração "${result.projectId}" aponta para marcador `
|
|
133
|
-
+ `"${found.marker?.projectId || 'ausente'}" em "${found.markerPath}".`,
|
|
134
|
-
);
|
|
135
|
-
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function resolveProjectVault({
|
|
141
|
-
input = {},
|
|
142
|
-
startDir = '',
|
|
143
|
-
explicitVault = '',
|
|
144
|
-
allowLegacySettings = true,
|
|
145
|
-
validateIdentity = true,
|
|
146
|
-
} = {}) {
|
|
147
|
-
const start = inputStart(input, startDir);
|
|
148
|
-
const explicit = explicitVault || input?.obsidian_vault_path;
|
|
149
|
-
if (explicit) {
|
|
150
|
-
return {
|
|
151
|
-
base: isAbsolute(explicit) ? resolve(explicit) : resolve(startDirectory(start), explicit),
|
|
152
|
-
source: explicitVault ? 'explicit' : 'payload',
|
|
153
|
-
projectRoot: startDirectory(start),
|
|
154
|
-
projectId: '',
|
|
155
|
-
configPath: '',
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const binding = findProjectBinding(start);
|
|
160
|
-
if (binding) {
|
|
161
|
-
const result = {
|
|
162
|
-
base: binding.base,
|
|
163
|
-
source: 'project-config',
|
|
164
|
-
projectRoot: binding.projectRoot,
|
|
165
|
-
projectId: binding.config.projectId,
|
|
166
|
-
configPath: binding.configPath,
|
|
167
|
-
};
|
|
168
|
-
if (validateIdentity) validateMarker(result);
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (allowLegacySettings) {
|
|
173
|
-
const legacy = findLegacyProjectVault(start);
|
|
174
|
-
if (legacy) return legacy;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const error = new Error(
|
|
178
|
-
`Nenhum vault WendKeep vinculado ao projeto em "${startDirectory(start)}". `
|
|
179
|
-
+ `Crie ${PROJECT_CONFIG_FILE} com \`wendkeep init --project "${startDirectory(start)}" --vault <path> --yes\`.`,
|
|
180
|
-
);
|
|
181
|
-
error.code = 'WENDKEEP_VAULT_UNCONFIGURED';
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function portableVaultPath(projectRoot, vaultPath) {
|
|
186
|
-
const rel = relative(projectRoot, vaultPath);
|
|
187
|
-
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return rel.replaceAll('\\', '/');
|
|
188
|
-
return vaultPath;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function bindProjectVault({ projectRoot, vaultPath }) {
|
|
192
|
-
const root = resolve(projectRoot);
|
|
193
|
-
const base = isAbsolute(vaultPath) ? resolve(vaultPath) : resolve(root, vaultPath);
|
|
194
|
-
const existing = readProjectBinding(root);
|
|
195
|
-
const existingMarker = readVaultMarker(base);
|
|
196
|
-
const projectId = existing?.config?.projectId || existingMarker?.marker?.projectId || randomUUID();
|
|
197
|
-
|
|
198
|
-
if (existingMarker?.marker?.projectId && existingMarker.marker.projectId !== projectId) {
|
|
199
|
-
const error = new Error(
|
|
200
|
-
`Não é seguro vincular "${root}" ao vault de outro projeto: `
|
|
201
|
-
+ `esperado "${projectId}", encontrado "${existingMarker.marker.projectId}".`,
|
|
202
|
-
);
|
|
203
|
-
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
mkdirSync(join(base, '.brain'), { recursive: true });
|
|
208
|
-
const config = {
|
|
209
|
-
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
210
|
-
projectId,
|
|
211
|
-
vault: portableVaultPath(root, base),
|
|
212
|
-
};
|
|
213
|
-
const marker = {
|
|
214
|
-
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
215
|
-
projectId,
|
|
216
|
-
projectName: basename(root),
|
|
217
|
-
};
|
|
218
|
-
atomicJson(join(base, ...PROJECT_MARKER_REL.split('/')), marker);
|
|
219
|
-
atomicJson(join(root, PROJECT_CONFIG_FILE), config);
|
|
220
|
-
return { base, projectRoot: root, projectId, config, marker };
|
|
221
|
-
}
|
|
1
|
+
// Compatibility facade during the physical package migration.
|
|
2
|
+
export * from '../packages/vault/src/project-vault.mjs';
|
package/src/rebuild-costs.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
6
6
|
import { updateSessionObservability } from '../hooks/session-observability.mjs';
|
|
7
|
+
import { assertVaultPathSafe } from '../hooks/vault-path-safety.mjs';
|
|
7
8
|
|
|
8
9
|
export function rebuildSessionCosts(vaultBase, { apply = false, session = '', limit = 0 } = {}) {
|
|
9
10
|
const registry = readSessionRegistry(vaultBase);
|
|
@@ -14,14 +15,20 @@ export function rebuildSessionCosts(vaultBase, { apply = false, session = '', li
|
|
|
14
15
|
for (const entry of entries) {
|
|
15
16
|
if (limit && report.scanned >= limit) break;
|
|
16
17
|
report.scanned += 1;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const checkedNote = assertVaultPathSafe(vaultBase, join(vaultBase, entry.session_file), {
|
|
19
|
+
expectedType: 'file', label: 'nota de sessão do rebuild de custos',
|
|
20
|
+
});
|
|
21
|
+
const note = checkedNote.target;
|
|
22
|
+
if (!entry.transcript_path || !checkedNote.exists || !existsSync(entry.transcript_path)) {
|
|
23
|
+
report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: checkedNote.exists, transcript: !!entry.transcript_path && existsSync(entry.transcript_path), transcriptPath: entry.transcript_path || '' });
|
|
20
24
|
continue;
|
|
21
25
|
}
|
|
22
26
|
const before = readFileSync(note, 'utf8');
|
|
23
27
|
try {
|
|
24
|
-
updateSessionObservability({
|
|
28
|
+
updateSessionObservability({
|
|
29
|
+
vaultBase, sessionPath: note, transcriptPath: entry.transcript_path,
|
|
30
|
+
caller: 'cost-rebuild', canonicalConversationId: entry.sessionId,
|
|
31
|
+
});
|
|
25
32
|
const after = readFileSync(note, 'utf8');
|
|
26
33
|
const changed = before !== after;
|
|
27
34
|
if (changed) report.changed += 1; else report.unchanged += 1;
|