@mnemonik/shared 7.97.2 → 7.98.1
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/dist/ambientPointer.d.ts.map +1 -1
- package/dist/ambientPointer.js +2 -1
- package/dist/ambientPointer.js.map +1 -1
- package/dist/apiOrigin.d.ts +3 -0
- package/dist/apiOrigin.d.ts.map +1 -0
- package/dist/apiOrigin.js +5 -0
- package/dist/apiOrigin.js.map +1 -0
- package/dist/ast/astChunker.d.ts +12 -12
- package/dist/ast/astChunker.js +32 -32
- package/dist/ast/grammars.d.ts +9 -9
- package/dist/ast/grammars.js +15 -15
- package/dist/codeScanner.d.ts +56 -43
- package/dist/codeScanner.d.ts.map +1 -1
- package/dist/codeScanner.js +262 -143
- package/dist/codeScanner.js.map +1 -1
- package/dist/contextBudget.js +2 -2
- package/dist/contextBudget.js.map +1 -1
- package/dist/gitEditDetector.d.ts +6 -6
- package/dist/gitEditDetector.js +7 -7
- package/dist/hookRuntime.d.ts +35 -2
- package/dist/hookRuntime.d.ts.map +1 -1
- package/dist/hookRuntime.js +127 -33
- package/dist/hookRuntime.js.map +1 -1
- package/dist/hookTimeouts.d.ts +6 -6
- package/dist/hookTimeouts.js +6 -6
- package/dist/hostAdapter.d.ts +135 -0
- package/dist/hostAdapter.d.ts.map +1 -0
- package/dist/hostAdapter.js +323 -0
- package/dist/hostAdapter.js.map +1 -0
- package/dist/hostBinary.d.ts +29 -0
- package/dist/hostBinary.d.ts.map +1 -0
- package/dist/hostBinary.js +179 -0
- package/dist/hostBinary.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +26 -26
- package/dist/instructions.js +26 -26
- package/dist/jitDeliveryAck.d.ts.map +1 -1
- package/dist/jitDeliveryAck.js +2 -0
- package/dist/jitDeliveryAck.js.map +1 -1
- package/dist/projectIdentityFile.d.ts +25 -0
- package/dist/projectIdentityFile.d.ts.map +1 -0
- package/dist/projectIdentityFile.js +87 -0
- package/dist/projectIdentityFile.js.map +1 -0
- package/dist/projectSetupHandoff.d.ts +24 -0
- package/dist/projectSetupHandoff.d.ts.map +1 -0
- package/dist/projectSetupHandoff.js +249 -0
- package/dist/projectSetupHandoff.js.map +1 -0
- package/dist/protectedPaths.d.ts +9 -0
- package/dist/protectedPaths.d.ts.map +1 -0
- package/dist/protectedPaths.js +66 -0
- package/dist/protectedPaths.js.map +1 -0
- package/dist/readiness.d.ts +143 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +304 -0
- package/dist/readiness.js.map +1 -0
- package/dist/repositoryFingerprint.d.ts +50 -0
- package/dist/repositoryFingerprint.d.ts.map +1 -0
- package/dist/repositoryFingerprint.js +160 -0
- package/dist/repositoryFingerprint.js.map +1 -0
- package/dist/repositoryRoot.d.ts +56 -0
- package/dist/repositoryRoot.d.ts.map +1 -0
- package/dist/repositoryRoot.js +226 -0
- package/dist/repositoryRoot.js.map +1 -0
- package/dist/runtimeReader.d.ts +102 -0
- package/dist/runtimeReader.d.ts.map +1 -0
- package/dist/runtimeReader.js +408 -0
- package/dist/runtimeReader.js.map +1 -0
- package/dist/runtimeSigners.d.ts +37 -0
- package/dist/runtimeSigners.d.ts.map +1 -0
- package/dist/runtimeSigners.js +507 -0
- package/dist/runtimeSigners.js.map +1 -0
- package/dist/scannerSupervisor.d.ts +35 -0
- package/dist/scannerSupervisor.d.ts.map +1 -0
- package/dist/scannerSupervisor.js +2 -0
- package/dist/scannerSupervisor.js.map +1 -0
- package/dist/secretPatterns.d.ts +9 -9
- package/dist/secretPatterns.d.ts.map +1 -1
- package/dist/secretPatterns.js +27 -25
- package/dist/secretPatterns.js.map +1 -1
- package/dist/testing/hostAdapterConformance.d.ts +17 -0
- package/dist/testing/hostAdapterConformance.d.ts.map +1 -0
- package/dist/testing/hostAdapterConformance.js +477 -0
- package/dist/testing/hostAdapterConformance.js.map +1 -0
- package/dist/usageGuide.d.ts +1 -1
- package/dist/usageGuide.d.ts.map +1 -1
- package/dist/usageGuide.js +4 -4
- package/package.json +7 -2
- package/queries/bash.tags.scm +1 -1
- package/queries/groovy.tags.scm +1 -1
- package/scripts/vendor-grammars.ts +13 -13
- package/src/ambientPointer.ts +2 -1
- package/src/apiOrigin.ts +4 -0
- package/src/ast/astChunker.ts +39 -39
- package/src/ast/grammars.ts +16 -16
- package/src/codeScanner.ts +274 -149
- package/src/contextBudget.ts +2 -2
- package/src/gitEditDetector.ts +7 -7
- package/src/hookRuntime.ts +162 -34
- package/src/hookTimeouts.ts +6 -6
- package/src/hostAdapter.ts +468 -0
- package/src/hostBinary.ts +205 -0
- package/src/index.ts +20 -0
- package/src/instructions.ts +26 -26
- package/src/jitDeliveryAck.ts +2 -0
- package/src/projectIdentityFile.ts +100 -0
- package/src/projectSetupHandoff.ts +275 -0
- package/src/protectedPaths.ts +88 -0
- package/src/readiness.ts +516 -0
- package/src/repositoryFingerprint.ts +199 -0
- package/src/repositoryRoot.ts +261 -0
- package/src/runtimeReader.ts +495 -0
- package/src/runtimeSigners.ts +577 -0
- package/src/scannerSupervisor.ts +26 -0
- package/src/secretPatterns.ts +27 -25
- package/src/testing/hostAdapterConformance.ts +538 -0
- package/src/usageGuide.ts +4 -4
- package/test-fixtures/identity/runner.d.mts +12 -0
- package/test-fixtures/identity/runner.mjs +282 -0
- package/tests/fixtures/repository-fingerprint-v1.golden.json +834 -0
- package/tests/projectIdentity.test.ts +56 -0
- package/tests/readiness.test.ts +158 -0
- package/tests/repositoryFingerprint.test.ts +26 -0
- package/tsconfig.test.json +9 -0
- package/wasm/bash.tags.scm +1 -1
- package/wasm/groovy.tags.scm +1 -1
- package/wasm/javascript.tags.scm +2 -2
- package/wasm/solidity.tags.scm +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, win32 } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import type { AdapterDependencies, FileChange, HostAdapter, Target } from '../hostAdapter.js';
|
|
6
|
+
|
|
7
|
+
export async function writeChanges(changes: FileChange[]) {
|
|
8
|
+
for (const change of changes) {
|
|
9
|
+
if (change.remove) {
|
|
10
|
+
await rm(change.path, { force: true });
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
await mkdir(dirname(change.path), { recursive: true });
|
|
14
|
+
await writeFile(change.path, change.content, { mode: change.mode });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function snapshot(root: string): Promise<Record<string, string>> {
|
|
18
|
+
const files: Record<string, string> = {};
|
|
19
|
+
for (const entry of await readdir(root, { recursive: true, withFileTypes: true })) {
|
|
20
|
+
const path = join(entry.parentPath, entry.name);
|
|
21
|
+
files[path] = entry.isDirectory() ? '<directory>' : (await readFile(path)).toString('base64');
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Named, isolated checks that any package test runner can register. No test-framework dependency. */
|
|
27
|
+
export function hostAdapterConformance(
|
|
28
|
+
create: (deps?: AdapterDependencies) => HostAdapter,
|
|
29
|
+
options: {
|
|
30
|
+
name: string;
|
|
31
|
+
config: string;
|
|
32
|
+
mcpConfig?: { user: string; project: string; toml?: boolean; type?: string };
|
|
33
|
+
scopes: Target['scope'][];
|
|
34
|
+
flat?: boolean;
|
|
35
|
+
deferred?: boolean;
|
|
36
|
+
}
|
|
37
|
+
): Record<string, () => Promise<void>> {
|
|
38
|
+
const foreign = { type: 'command', command: 'node /foreign/hook.js', custom: ' keep exactly ' };
|
|
39
|
+
const event = options.flat ? 'preToolUse' : 'PreToolUse';
|
|
40
|
+
const foreignEntry = options.flat ? foreign : { matcher: '.*', hooks: [foreign], custom: 'keep' };
|
|
41
|
+
const fixture = async () => {
|
|
42
|
+
const root = await mkdtemp(join(tmpdir(), 'mnemonik-adapter-'));
|
|
43
|
+
const home = join(root, 'home');
|
|
44
|
+
const project = join(root, 'project');
|
|
45
|
+
await mkdir(home);
|
|
46
|
+
await mkdir(project);
|
|
47
|
+
await mkdir(join(root, 'empty-bin'));
|
|
48
|
+
const runtimeRoot = join(home, '.mnemonik', 'runtimes', options.name);
|
|
49
|
+
const target: Target = {
|
|
50
|
+
component: 'hooks',
|
|
51
|
+
scope: 'user',
|
|
52
|
+
projectRoot: project,
|
|
53
|
+
runtimeRoot,
|
|
54
|
+
credentialFamily: 'hook-family',
|
|
55
|
+
runtimeEntry: join(runtimeRoot, '1.0.0', 'dist', 'hook.js'),
|
|
56
|
+
};
|
|
57
|
+
const deps: AdapterDependencies = {
|
|
58
|
+
target,
|
|
59
|
+
platform: 'linux',
|
|
60
|
+
env: { HOME: home, USERPROFILE: home, PATH: join(root, 'empty-bin') },
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
artifactDigest: 'verified-receipt',
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
root,
|
|
66
|
+
project,
|
|
67
|
+
target,
|
|
68
|
+
deps,
|
|
69
|
+
adapter: create(deps),
|
|
70
|
+
configPath: (scope: Target['scope']) =>
|
|
71
|
+
join(scope === 'user' ? home : project, options.config),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const checks: Record<string, () => Promise<void>> = {};
|
|
75
|
+
const check = (name: string, run: (f: Awaited<ReturnType<typeof fixture>>) => Promise<void>) => {
|
|
76
|
+
checks[`${options.name}: ${name}`] = async () => {
|
|
77
|
+
const f = await fixture();
|
|
78
|
+
try {
|
|
79
|
+
await run(f);
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(f.root, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const assertForeign = async (path: string) => {
|
|
86
|
+
const raw = await readFile(path, 'utf8');
|
|
87
|
+
const config = JSON.parse(raw);
|
|
88
|
+
assert.equal(JSON.stringify(config.hooks[event][0]), JSON.stringify(foreignEntry));
|
|
89
|
+
const fragment = JSON.stringify(foreignEntry, null, 2)
|
|
90
|
+
.split('\n')
|
|
91
|
+
.map((line, index) => (index ? ` ${line}` : line))
|
|
92
|
+
.join('\n');
|
|
93
|
+
assert.ok(raw.includes(fragment), 'foreign entry bytes must survive');
|
|
94
|
+
};
|
|
95
|
+
check(
|
|
96
|
+
'detect reports every searched location and writes nothing with host binaries absent',
|
|
97
|
+
async ({ root, adapter }) => {
|
|
98
|
+
const before = await snapshot(root);
|
|
99
|
+
const { resolvedPath: _resolvedPath, ...detected } = await adapter.detect();
|
|
100
|
+
assert.deepEqual(detected, {
|
|
101
|
+
supported: false,
|
|
102
|
+
version: '',
|
|
103
|
+
reason: options.deferred ? 'deferred' : 'not_found',
|
|
104
|
+
...(options.deferred
|
|
105
|
+
? {}
|
|
106
|
+
: {
|
|
107
|
+
searchedLocations: [
|
|
108
|
+
...(options.name === 'cursor'
|
|
109
|
+
? ['/usr/share/cursor/bin/cursor', '/opt/Cursor/resources/app/bin/cursor']
|
|
110
|
+
: []),
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
assert.deepEqual(await snapshot(root), before);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
if (options.deferred) {
|
|
118
|
+
check('defers every other operation', async ({ adapter, target }) => {
|
|
119
|
+
for (const operation of [
|
|
120
|
+
() => adapter.capabilities(),
|
|
121
|
+
() => adapter.inspect(),
|
|
122
|
+
() => adapter.plan(),
|
|
123
|
+
() => adapter.install({ stage: async () => {} }, target),
|
|
124
|
+
() => adapter.update({ stage: async () => {} }, target),
|
|
125
|
+
() => adapter.repair({ stage: async () => {} }, target),
|
|
126
|
+
() => adapter.launch(),
|
|
127
|
+
() => adapter.verify(),
|
|
128
|
+
() => adapter.uninstall({ stage: async () => {} }, target),
|
|
129
|
+
() => adapter.revoke?.({ id: 'grant', account: 'account', scopes: [] }),
|
|
130
|
+
])
|
|
131
|
+
await assert.rejects(async (): Promise<unknown> => operation(), /not_supported/);
|
|
132
|
+
});
|
|
133
|
+
return checks;
|
|
134
|
+
}
|
|
135
|
+
const versionOutput = {
|
|
136
|
+
cursor: '3.20.17',
|
|
137
|
+
grok: 'grok 1.0.25',
|
|
138
|
+
'claude-code': '2.1.268 (Claude Code)',
|
|
139
|
+
codex: 'codex-cli 0.145.0',
|
|
140
|
+
}[options.name];
|
|
141
|
+
assert.ok(versionOutput);
|
|
142
|
+
check('distinguishes wrong vendor output from an absent binary', async ({ deps }) => {
|
|
143
|
+
const adapter = create({
|
|
144
|
+
...deps,
|
|
145
|
+
execFile: async () => ({ stdout: 'another-product 1.2.3', stderr: '' }),
|
|
146
|
+
});
|
|
147
|
+
assert.deepEqual(await adapter.detect(), {
|
|
148
|
+
supported: false,
|
|
149
|
+
version: '1.2.3',
|
|
150
|
+
reason: 'wrong_vendor',
|
|
151
|
+
resolvedPath: options.name === 'claude-code' ? 'claude' : options.name,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
check('keeps unrecognised vendor output as an unverified version', async ({ deps }) => {
|
|
155
|
+
const adapter = create({
|
|
156
|
+
...deps,
|
|
157
|
+
execFile: async () => ({ stdout: '', stderr: '' }),
|
|
158
|
+
});
|
|
159
|
+
assert.deepEqual(await adapter.detect(), {
|
|
160
|
+
supported: false,
|
|
161
|
+
version: '',
|
|
162
|
+
reason: 'unverified_version',
|
|
163
|
+
resolvedPath: options.name === 'claude-code' ? 'claude' : options.name,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
check(
|
|
167
|
+
'Windows prefers vendor installation, then filters ordered where candidates and caches the result',
|
|
168
|
+
async ({ deps, target }) => {
|
|
169
|
+
const name = options.name === 'claude-code' ? 'claude' : options.name;
|
|
170
|
+
const extension = options.name === 'grok' ? '.exe' : '.cmd';
|
|
171
|
+
const preferred =
|
|
172
|
+
options.name === 'cursor'
|
|
173
|
+
? 'C:/Local/Programs/cursor/resources/app/bin/cursor.cmd'
|
|
174
|
+
: options.name === 'grok'
|
|
175
|
+
? 'C:/User/.grok/bin/grok.exe'
|
|
176
|
+
: `C:/Roaming/npm/${name}.cmd`;
|
|
177
|
+
const normalize = win32.normalize;
|
|
178
|
+
for (const installed of [true, false]) {
|
|
179
|
+
const fallback = normalize(`C:/First Bin/${name}${extension}`);
|
|
180
|
+
const expected = installed ? normalize(preferred) : fallback;
|
|
181
|
+
const calls: string[][] = [];
|
|
182
|
+
const host = create({
|
|
183
|
+
...deps,
|
|
184
|
+
platform: 'win32',
|
|
185
|
+
target: { ...target, component: 'mcp' },
|
|
186
|
+
env: {
|
|
187
|
+
...deps.env,
|
|
188
|
+
LOCALAPPDATA: 'C:/Local',
|
|
189
|
+
USERPROFILE: 'C:/User',
|
|
190
|
+
APPDATA: 'C:/Roaming',
|
|
191
|
+
ComSpec: 'C:/Windows/System32/cmd.exe',
|
|
192
|
+
},
|
|
193
|
+
binaryExists: async (file) => installed || file === fallback || file.includes('Second'),
|
|
194
|
+
execFile: async (file, args) => {
|
|
195
|
+
calls.push([file, ...args]);
|
|
196
|
+
return {
|
|
197
|
+
stdout: file.endsWith('where.exe')
|
|
198
|
+
? `${name}${extension}\r\nC:\\Wrong\\${name}.ps1\r\n${fallback}\r\nC:\\Second\\${name}${extension}`
|
|
199
|
+
: args.join(' ').includes('--version')
|
|
200
|
+
? versionOutput
|
|
201
|
+
: 'mnemonik: Connected',
|
|
202
|
+
stderr: '',
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const [first, second] = await Promise.all([host.detect(), host.detect()]);
|
|
207
|
+
assert.equal(first.supported, true);
|
|
208
|
+
assert.equal(second.resolvedPath, expected);
|
|
209
|
+
assert.equal((await host.inspect()).resolvedPath, expected);
|
|
210
|
+
assert.equal(
|
|
211
|
+
calls.filter(([file]) => file.endsWith('where.exe')).length,
|
|
212
|
+
installed ? 0 : 1
|
|
213
|
+
);
|
|
214
|
+
assert.equal(calls.filter((call) => call.join(' ').includes('--version')).length, 1);
|
|
215
|
+
for (const [file, ...args] of calls.filter(([file]) => !file.endsWith('where.exe'))) {
|
|
216
|
+
if (extension === '.exe') assert.equal(file, expected);
|
|
217
|
+
else {
|
|
218
|
+
assert.equal(file, 'C:/Windows/System32/cmd.exe');
|
|
219
|
+
assert.deepEqual(args.slice(0, 4), ['/d', '/s', '/c', `""${expected}"`]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
check('reports real scopes and leaves unobserved grants absent', async ({ adapter }) => {
|
|
226
|
+
assert.deepEqual(adapter.capabilities(), {
|
|
227
|
+
scopes: options.scopes,
|
|
228
|
+
revoke: ['claude-code', 'codex'].includes(options.name),
|
|
229
|
+
components: ['hooks', 'mcp'],
|
|
230
|
+
nativeConnect: !['grok', 'claude-code', 'cursor'].includes(options.name),
|
|
231
|
+
...(options.name === 'cursor' ? { nativeListing: false } : {}),
|
|
232
|
+
});
|
|
233
|
+
const inspection = await adapter.verify();
|
|
234
|
+
assert.equal(inspection.declarationPresent, false);
|
|
235
|
+
assert.equal(inspection.authenticatedTools, false);
|
|
236
|
+
assert.equal(inspection.grant, undefined);
|
|
237
|
+
assert.match(await adapter.launch(), /mnemonik/i);
|
|
238
|
+
});
|
|
239
|
+
check('rejects shell metacharacters in the configured origin', async ({ deps }) => {
|
|
240
|
+
const adapter = create({
|
|
241
|
+
...deps,
|
|
242
|
+
env: { ...deps.env, MNEMONIK_API_RESOURCE: 'https://x;calc.test' },
|
|
243
|
+
});
|
|
244
|
+
await assert.rejects(() => adapter.plan(), /invalid_server_origin/);
|
|
245
|
+
});
|
|
246
|
+
for (const scope of options.scopes)
|
|
247
|
+
check(
|
|
248
|
+
`${scope}: deterministic plan; install stages and does not write; uninstall preserves foreign bytes`,
|
|
249
|
+
async ({ root, target: base, deps, configPath }) => {
|
|
250
|
+
const adapter = create({
|
|
251
|
+
...deps,
|
|
252
|
+
env: { ...deps.env, MNEMONIK_API_RESOURCE: 'https://staging.example.test/api' },
|
|
253
|
+
});
|
|
254
|
+
const target = { ...base, scope };
|
|
255
|
+
await writeChanges([
|
|
256
|
+
{
|
|
257
|
+
path: configPath(scope),
|
|
258
|
+
content: Buffer.from(
|
|
259
|
+
`${JSON.stringify({ hooks: { [event]: [foreignEntry] } }, null, 2)}\n`
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
const before = await snapshot(root);
|
|
264
|
+
const plan = await adapter.plan(target);
|
|
265
|
+
assert.deepEqual(await adapter.plan(target), plan);
|
|
266
|
+
assert.equal(plan.requestedScope, scope);
|
|
267
|
+
assert.equal(plan.effectiveScope, scope);
|
|
268
|
+
assert.equal(plan.version, '1.0.0');
|
|
269
|
+
assert.equal(plan.artifactDigest, 'verified-receipt');
|
|
270
|
+
assert.ok(
|
|
271
|
+
plan.changes.some((change) =>
|
|
272
|
+
change.content.includes('--credential-family ' + target.credentialFamily)
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
const configuration = plan.changes.at(-1);
|
|
276
|
+
assert.ok(configuration);
|
|
277
|
+
const config = JSON.parse(configuration.content.toString());
|
|
278
|
+
const commands: string[] = [];
|
|
279
|
+
const collect = (value: unknown): void => {
|
|
280
|
+
if (!value || typeof value !== 'object') return;
|
|
281
|
+
for (const [key, child] of Object.entries(value)) {
|
|
282
|
+
if (
|
|
283
|
+
key === 'command' &&
|
|
284
|
+
typeof child === 'string' &&
|
|
285
|
+
child.includes('--mnemonik-owner=')
|
|
286
|
+
)
|
|
287
|
+
commands.push(child);
|
|
288
|
+
else collect(child);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
collect(config);
|
|
292
|
+
assert.ok(commands.length > 0);
|
|
293
|
+
for (const command of commands)
|
|
294
|
+
assert.ok(command.includes(' --server https://staging.example.test '), command);
|
|
295
|
+
if (options.name !== 'codex') {
|
|
296
|
+
const config = plan.changes.at(-1);
|
|
297
|
+
assert.ok(config);
|
|
298
|
+
const hooks = JSON.parse(config.content.toString()).hooks;
|
|
299
|
+
const event = options.name === 'cursor' ? 'afterMCPExecution' : 'PostToolUse';
|
|
300
|
+
assert.ok(
|
|
301
|
+
hooks[event]
|
|
302
|
+
.flatMap((entry: { hooks?: unknown[] }) => entry.hooks ?? [entry])
|
|
303
|
+
.some(
|
|
304
|
+
(entry: { command?: string; timeout?: number }) =>
|
|
305
|
+
entry.command?.includes('--credential-family ' + target.credentialFamily) &&
|
|
306
|
+
(entry.timeout ?? 0) >= 40
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (options.name !== 'codex')
|
|
311
|
+
assert.ok(plan.changes.some((c) => c.content.includes(target.runtimeEntry)));
|
|
312
|
+
else
|
|
313
|
+
assert.ok(plan.changes.some((c) => c.path === join(target.runtimeRoot, 'launcher.mjs')));
|
|
314
|
+
assert.deepEqual(await snapshot(root), before);
|
|
315
|
+
const staged: FileChange[] = [];
|
|
316
|
+
const writer = {
|
|
317
|
+
stage: async (change: FileChange) => {
|
|
318
|
+
staged.push(change);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
await adapter.install(writer, target);
|
|
322
|
+
assert.deepEqual(staged, plan.changes);
|
|
323
|
+
assert.deepEqual(await snapshot(root), before, 'install must not write');
|
|
324
|
+
await writeChanges(staged);
|
|
325
|
+
assert.equal((await adapter.inspect(target)).declarationPresent, true);
|
|
326
|
+
await assertForeign(configPath(scope));
|
|
327
|
+
const installed = await snapshot(root);
|
|
328
|
+
for (const operation of [adapter.update, adapter.repair]) {
|
|
329
|
+
staged.length = 0;
|
|
330
|
+
await operation(writer, target);
|
|
331
|
+
assert.deepEqual(staged, plan.changes);
|
|
332
|
+
assert.deepEqual(await snapshot(root), installed);
|
|
333
|
+
}
|
|
334
|
+
staged.length = 0;
|
|
335
|
+
await adapter.uninstall(writer, target);
|
|
336
|
+
assert.deepEqual(await snapshot(root), installed);
|
|
337
|
+
await writeChanges(staged);
|
|
338
|
+
assert.equal((await adapter.inspect(target)).declarationPresent, false);
|
|
339
|
+
assert.ok(!(await readFile(configPath(scope), 'utf8')).includes('mnemonik'));
|
|
340
|
+
await assertForeign(configPath(scope));
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
for (const scope of options.scopes)
|
|
344
|
+
for (const component of options.mcpConfig ? (['hooks', 'mcp'] as const) : (['hooks'] as const))
|
|
345
|
+
check(
|
|
346
|
+
`${scope} ${component}: uninstall removes only a config created by install`,
|
|
347
|
+
async ({ target: base, deps, project, configPath }) => {
|
|
348
|
+
const target = { ...base, component, scope };
|
|
349
|
+
const path =
|
|
350
|
+
component === 'hooks'
|
|
351
|
+
? configPath(scope)
|
|
352
|
+
: join(
|
|
353
|
+
scope === 'user' ? (deps.env?.HOME ?? '') : project,
|
|
354
|
+
options.mcpConfig?.[scope] ?? ''
|
|
355
|
+
);
|
|
356
|
+
target.createdFiles = [path];
|
|
357
|
+
const adapter = create({ ...deps, target });
|
|
358
|
+
await writeChanges((await adapter.plan()).changes);
|
|
359
|
+
const staged: FileChange[] = [];
|
|
360
|
+
await adapter.uninstall({ stage: async (change) => void staged.push(change) }, target);
|
|
361
|
+
await writeChanges(staged);
|
|
362
|
+
await assert.rejects(readFile(path), { code: 'ENOENT' });
|
|
363
|
+
|
|
364
|
+
const empty = component === 'mcp' && options.mcpConfig?.toml ? '\n' : '{}\n';
|
|
365
|
+
await writeFile(path, empty);
|
|
366
|
+
const preExistingTarget = { ...base, component, scope };
|
|
367
|
+
const preExisting = create({ ...deps, target: preExistingTarget });
|
|
368
|
+
await writeChanges((await preExisting.plan()).changes);
|
|
369
|
+
staged.length = 0;
|
|
370
|
+
await preExisting.uninstall(
|
|
371
|
+
{ stage: async (change) => void staged.push(change) },
|
|
372
|
+
preExistingTarget
|
|
373
|
+
);
|
|
374
|
+
await writeChanges(staged);
|
|
375
|
+
assert.ok(!(await readFile(path, 'utf8')).includes('mnemonik'));
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
check(
|
|
379
|
+
'replacement rule: reports alternate-scope entries and never stages that config',
|
|
380
|
+
async ({ adapter, target, configPath }) => {
|
|
381
|
+
await writeChanges((await adapter.plan(target)).changes);
|
|
382
|
+
const before = await readFile(configPath('user'));
|
|
383
|
+
const projectTarget = { ...target, scope: 'project' as const };
|
|
384
|
+
assert.deepEqual((await adapter.inspect(projectTarget)).otherScopes, [
|
|
385
|
+
{ scope: 'user', path: configPath('user') },
|
|
386
|
+
]);
|
|
387
|
+
assert.ok(
|
|
388
|
+
!(await adapter.plan(projectTarget)).changes.some((c) => c.path === configPath('user'))
|
|
389
|
+
);
|
|
390
|
+
await writeChanges((await adapter.plan(projectTarget)).changes);
|
|
391
|
+
assert.deepEqual(await readFile(configPath('user')), before);
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
check(
|
|
395
|
+
'parses connected listings without inferring account and uses execFile arguments',
|
|
396
|
+
async ({ deps, project, target }) => {
|
|
397
|
+
let stdout = 'mnemonik: https://api.mnemonik.dev/mcp - ✔ Connected\n';
|
|
398
|
+
const calls: unknown[][] = [];
|
|
399
|
+
const host = create({
|
|
400
|
+
...deps,
|
|
401
|
+
target: { ...target, component: 'mcp' },
|
|
402
|
+
execFile: async (...args) => {
|
|
403
|
+
if (args[1][0] === '--version') return { stdout: versionOutput, stderr: '' };
|
|
404
|
+
calls.push(args);
|
|
405
|
+
return { stdout, stderr: '' };
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const inspection = await host.verify();
|
|
409
|
+
if (options.name === 'cursor') {
|
|
410
|
+
assert.equal(inspection.authenticatedTools, false);
|
|
411
|
+
assert.equal(inspection.grant, undefined);
|
|
412
|
+
assert.deepEqual(calls, []);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
assert.equal(inspection.authenticatedTools, true);
|
|
416
|
+
assert.equal(inspection.grant, undefined);
|
|
417
|
+
assert.deepEqual(calls[0], [
|
|
418
|
+
options.name === 'claude-code'
|
|
419
|
+
? 'claude'
|
|
420
|
+
: options.name === 'cursor'
|
|
421
|
+
? 'agent'
|
|
422
|
+
: options.name,
|
|
423
|
+
['mcp', 'list'],
|
|
424
|
+
{ env: deps.env, cwd: project, timeout: 5000, maxBuffer: 64 * 1024, windowsHide: true },
|
|
425
|
+
]);
|
|
426
|
+
for (const listing of [
|
|
427
|
+
'mnemonik: not connected',
|
|
428
|
+
'foreign: ✔ Connected',
|
|
429
|
+
'mnemonik: enabled OAuth',
|
|
430
|
+
'mnemonik-foreign: Connected',
|
|
431
|
+
'mnemonik: warning: was previously connected',
|
|
432
|
+
'unrecognized format',
|
|
433
|
+
]) {
|
|
434
|
+
stdout = listing;
|
|
435
|
+
assert.equal((await host.verify()).authenticatedTools, false);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
if (options.mcpConfig)
|
|
440
|
+
for (const scope of options.scopes)
|
|
441
|
+
check(
|
|
442
|
+
`${scope}: MCP URL only, foreign server preserved, staged lifecycle`,
|
|
443
|
+
async ({ root, project, deps, target: base }) => {
|
|
444
|
+
const target = { ...base, component: 'mcp' as const, scope };
|
|
445
|
+
const adapter = create({ ...deps, target });
|
|
446
|
+
const config = options.mcpConfig;
|
|
447
|
+
assert.ok(config);
|
|
448
|
+
const path = join(scope === 'user' ? (deps.env?.HOME ?? '') : project, config[scope]);
|
|
449
|
+
const foreign = { url: 'https://foreign.example/mcp', custom: ' keep exactly ' };
|
|
450
|
+
const original = config.toml
|
|
451
|
+
? '[mcp_servers.foreign]\nurl = "https://foreign.example/mcp"\ncustom = " keep exactly "\n'
|
|
452
|
+
: JSON.stringify({ mcpServers: { foreign }, setting: 'keep' }, null, 2) + '\n';
|
|
453
|
+
await writeChanges([{ path, content: Buffer.from(original) }]);
|
|
454
|
+
const before = await snapshot(root);
|
|
455
|
+
const plan = await adapter.plan();
|
|
456
|
+
assert.equal(plan.changes.length, 1);
|
|
457
|
+
assert.equal(plan.changes[0]?.path, path);
|
|
458
|
+
const raw = plan.changes[0]?.content.toString();
|
|
459
|
+
assert.ok(raw);
|
|
460
|
+
assert.ok(raw.includes('https://api.mnemonik.dev/mcp'));
|
|
461
|
+
const sessionHeader = 'headers = { "x-mcp-session-id" = "{{session_id}}" }\n';
|
|
462
|
+
const withoutSessionHeader =
|
|
463
|
+
options.name === 'grok' && config.toml ? raw.replace(sessionHeader, '') : raw;
|
|
464
|
+
if (options.name === 'grok') assert.ok(raw.includes(sessionHeader));
|
|
465
|
+
assert.doesNotMatch(
|
|
466
|
+
withoutSessionHeader,
|
|
467
|
+
/token|secret|headers|credential|bearer|runtime|client.?id/i
|
|
468
|
+
);
|
|
469
|
+
if (config.toml) assert.ok(raw.startsWith(original));
|
|
470
|
+
else {
|
|
471
|
+
const parsed = JSON.parse(raw);
|
|
472
|
+
assert.deepEqual(parsed.mcpServers.foreign, foreign);
|
|
473
|
+
assert.deepEqual(parsed.mcpServers.mnemonik, {
|
|
474
|
+
...(config.type ? { type: config.type } : {}),
|
|
475
|
+
url: 'https://api.mnemonik.dev/mcp',
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const staged: FileChange[] = [];
|
|
479
|
+
const writer = {
|
|
480
|
+
stage: async (change: FileChange) => {
|
|
481
|
+
staged.push(change);
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
await adapter.install(writer, target);
|
|
485
|
+
assert.deepEqual(staged, plan.changes);
|
|
486
|
+
assert.deepEqual(await snapshot(root), before);
|
|
487
|
+
await writeChanges(staged);
|
|
488
|
+
assert.equal((await adapter.inspect()).declarationPresent, true);
|
|
489
|
+
assert.deepEqual((await adapter.plan()).changes, plan.changes);
|
|
490
|
+
staged.length = 0;
|
|
491
|
+
await adapter.uninstall(writer, target);
|
|
492
|
+
await writeChanges(staged);
|
|
493
|
+
const removed = await readFile(path, 'utf8');
|
|
494
|
+
assert.ok(!removed.includes('mnemonik'));
|
|
495
|
+
if (config.toml) assert.equal(removed, original);
|
|
496
|
+
else assert.deepEqual(JSON.parse(removed).mcpServers.foreign, foreign);
|
|
497
|
+
|
|
498
|
+
const sameName = config.toml
|
|
499
|
+
? '[mcp_servers.mnemonik]\nurl = "https://foreign.example/mcp"\ncustom = "keep"\n'
|
|
500
|
+
: JSON.stringify({ mcpServers: { mnemonik: foreign }, setting: 'keep' }, null, 2) +
|
|
501
|
+
'\n';
|
|
502
|
+
await writeFile(path, sameName);
|
|
503
|
+
const conflicted = await snapshot(root);
|
|
504
|
+
await assert.rejects(adapter.plan(), /mcp_name_conflict/);
|
|
505
|
+
await assert.rejects(
|
|
506
|
+
adapter.uninstall({ stage: async () => assert.fail('must not stage') }, target),
|
|
507
|
+
/mcp_name_conflict/
|
|
508
|
+
);
|
|
509
|
+
assert.deepEqual(await snapshot(root), conflicted);
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
check(
|
|
513
|
+
'native login/logout use the named server; missing executable returns manual instruction without writes',
|
|
514
|
+
async ({ root, deps }) => {
|
|
515
|
+
const calls: string[][] = [];
|
|
516
|
+
const adapter = create({
|
|
517
|
+
...deps,
|
|
518
|
+
execFile: async (_file, args) => {
|
|
519
|
+
if (args[0] === '--version') return { stdout: versionOutput, stderr: '' };
|
|
520
|
+
calls.push(args);
|
|
521
|
+
return { stdout: '', stderr: '' };
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
await adapter.launch();
|
|
525
|
+
if (adapter.capabilities().nativeConnect)
|
|
526
|
+
assert.deepEqual(calls.shift(), ['mcp', 'login', 'mnemonik']);
|
|
527
|
+
else assert.deepEqual(calls, []);
|
|
528
|
+
if (adapter.revoke) {
|
|
529
|
+
assert.equal(await adapter.revoke({ id: 'grant', account: 'owner', scopes: [] }), true);
|
|
530
|
+
assert.deepEqual(calls.shift(), ['mcp', 'logout', 'mnemonik']);
|
|
531
|
+
}
|
|
532
|
+
const before = await snapshot(root);
|
|
533
|
+
assert.match(await create(deps).launch(), /mnemonik/i);
|
|
534
|
+
assert.deepEqual(await snapshot(root), before);
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
return checks;
|
|
538
|
+
}
|
package/src/usageGuide.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const USAGE_GUIDE = `# Mnemonik Workflow Guide (v2.85)
|
|
|
15
15
|
|
|
16
16
|
## Workflow
|
|
17
17
|
|
|
18
|
-
session_bootstrap
|
|
18
|
+
session_bootstrap -> memory_search -> file_context -> [work] -> memory_add -> memory_state
|
|
19
19
|
|
|
20
20
|
## Tool Selection by Stage
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ session_bootstrap → memory_search → file_context → [work] → memory_add
|
|
|
26
26
|
- policy: review safety rules
|
|
27
27
|
|
|
28
28
|
### Before editing files
|
|
29
|
-
- file_context: fetch memories for the file
|
|
29
|
+
- file_context: fetch memories for the file - call for EVERY file you edit
|
|
30
30
|
- memory_search: second search scoped to file/module if needed
|
|
31
31
|
- mnemonik.docs({ action: 'drift', scopePath }): check doc-truth findings for the file
|
|
32
32
|
|
|
@@ -48,11 +48,11 @@ session_bootstrap → memory_search → file_context → [work] → memory_add
|
|
|
48
48
|
- memory_add: save decisions, outcomes, patterns, bug root causes
|
|
49
49
|
- memory_state: reinforce (memory helped), supersede (replace outdated), deprecate, penalize, dispute
|
|
50
50
|
- tasks: mark tasks in progress or complete
|
|
51
|
-
- mnemonik.docs({ action: 'status' }): view doc-truth health (legacy coupling counts are diagnostic only
|
|
51
|
+
- mnemonik.docs({ action: 'status' }): view doc-truth health (legacy coupling counts are diagnostic only - do not act on them as drift)
|
|
52
52
|
|
|
53
53
|
### Diagnostics
|
|
54
54
|
- doctor: when tool calls fail or behavior is inconsistent
|
|
55
|
-
- scanner: metrics (is the indexing daemon alive), history, drift. Nothing here starts indexing
|
|
55
|
+
- scanner: metrics (is the indexing daemon alive), history, drift. Nothing here starts indexing - the daemon owns it
|
|
56
56
|
|
|
57
57
|
## Skip conditions
|
|
58
58
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IdentityFixtureAdapter {
|
|
2
|
+
parse(text: string): unknown;
|
|
3
|
+
read(dir: string): Promise<unknown>;
|
|
4
|
+
resolveRoot(cwd: string): Promise<unknown>;
|
|
5
|
+
resolve(cwd: string, options?: { allowNestedInherit?: boolean }): Promise<unknown>;
|
|
6
|
+
find?(cwd: string): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function runIdentityFixtureSuite(
|
|
10
|
+
register: (name: string, run: () => void | Promise<void>) => unknown,
|
|
11
|
+
adapter: IdentityFixtureAdapter
|
|
12
|
+
): void;
|