@mnemonik/shared 7.97.2 → 7.98.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/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
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { USAGE_GUIDE } from './usageGuide.js';
|
|
|
10
10
|
export {
|
|
11
11
|
CodeScanner,
|
|
12
12
|
MAX_SCANNED_FILE_BYTES,
|
|
13
|
+
MAX_PUSH_CHUNK_CONTENT_LENGTH,
|
|
13
14
|
AUTHORITY_FILE_MATCHERS,
|
|
14
15
|
BUILT_IN_IGNORE_DIRS,
|
|
15
16
|
DEFAULT_INCLUDE_EXTENSIONS,
|
|
@@ -72,3 +73,22 @@ export {
|
|
|
72
73
|
} from './jitDeliveryAck.js';
|
|
73
74
|
export * from './settingsIo.js';
|
|
74
75
|
export * from './claudeProxySettings.js';
|
|
76
|
+
export * from './repositoryFingerprint.js';
|
|
77
|
+
export * from './projectIdentityFile.js';
|
|
78
|
+
export * from './repositoryRoot.js';
|
|
79
|
+
export * from './protectedPaths.js';
|
|
80
|
+
export * from './readiness.js';
|
|
81
|
+
|
|
82
|
+
export * from './hostAdapter.js';
|
|
83
|
+
export * from './hostBinary.js';
|
|
84
|
+
|
|
85
|
+
export type {
|
|
86
|
+
ServiceDefinition,
|
|
87
|
+
SupervisorStatus,
|
|
88
|
+
ServiceOperation,
|
|
89
|
+
ServiceResult,
|
|
90
|
+
} from './scannerSupervisor.js';
|
|
91
|
+
|
|
92
|
+
export { apiOrigin } from './apiOrigin.js';
|
|
93
|
+
|
|
94
|
+
export { WINDOWS_SERVICE_BUDGET_MS } from './scannerSupervisor.js';
|
package/src/instructions.ts
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* Version: 2.110
|
|
8
8
|
* Updated: 2026-08-27
|
|
9
9
|
*
|
|
10
|
-
* v2.110
|
|
10
|
+
* v2.110 - Reordered and compressed the universal instructions to fit the
|
|
11
11
|
* 2,048-character transport limit. The first 512 characters now carry
|
|
12
12
|
* the server purpose, self-scoping bootstrap condition, required cwd,
|
|
13
13
|
* and memory-as-augmentation stance. The retrieval, task pagination,
|
|
14
14
|
* and tool-floor capabilities remain in a shorter factual form.
|
|
15
15
|
*
|
|
16
|
-
* v2.109
|
|
16
|
+
* v2.109 - Compressed the v2.108 memory-search clause. It restated, verbatim,
|
|
17
17
|
* the same search guidance carried by SESSION_OPENER_INSTRUCTION
|
|
18
|
-
* (bootstrapDigestRender.ts)
|
|
18
|
+
* (bootstrapDigestRender.ts) - and both co-occur in the same agent
|
|
19
19
|
* session as the bootstrap digest's capabilities.search block, which
|
|
20
20
|
* already states the memory_search/code_search call signatures in
|
|
21
21
|
* full. Kept the essential behavior (memory holds the why; search it
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* with Grep/Read; reach for code_search when concepts don't share
|
|
24
24
|
* the user's words), dropped the restated call shapes, and pointed at
|
|
25
25
|
* the session context's capabilities block for the exact signatures
|
|
26
|
-
* once it loads
|
|
26
|
+
* once it loads - this text fires at `initialize`, before any
|
|
27
27
|
* digest exists, so it cannot point "above" the way the trimmed
|
|
28
28
|
* SESSION_OPENER clause does. recall-first-class plan, Task A3
|
|
29
29
|
* rider (reviewer-flagged: character-for-character duplicate).
|
|
30
|
-
* v2.108
|
|
30
|
+
* v2.108 - Added front-loaded education that persistent project memory is part
|
|
31
31
|
* of the agent's working knowledge. A live Config-B fixture rejected
|
|
32
32
|
* the first generic invitation: the agent searched code, docs, and git
|
|
33
33
|
* history, then called the rationale undocumented without searching
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* wording stays a compact startup floor, not a token-heavy tutorial or
|
|
39
39
|
* recurring nudge.
|
|
40
40
|
*
|
|
41
|
-
* v2.106
|
|
41
|
+
* v2.106 - Added a static "Which tool for what" floor map. Rides the already-
|
|
42
42
|
* cached instructions at near-zero cost and gives every agent a baseline
|
|
43
43
|
* "which tool for what" for the under-surfaced tools (memory_add,
|
|
44
44
|
* memory_state, memory_info, memory_links, assist, search_summaries,
|
|
@@ -46,55 +46,55 @@
|
|
|
46
46
|
* nudges; memory_info in particular has no reactive trigger, so the floor
|
|
47
47
|
* is its only education.
|
|
48
48
|
*
|
|
49
|
-
* v2.105
|
|
49
|
+
* v2.105 - Bootstrap's agent-facing label is generic project context. Use the
|
|
50
50
|
* canonical text body's stable `PROJECT_CONTEXT schemaVersion=` marker
|
|
51
51
|
* for the self-scoping check instead of teaching agents to call the
|
|
52
52
|
* user's project "Mnemonik".
|
|
53
53
|
*
|
|
54
|
-
* v2.104
|
|
54
|
+
* v2.104 - Disambiguated persistent project task memories from Claude Code's
|
|
55
55
|
* host-local TaskList and made completeness proactive. A request for a
|
|
56
56
|
* complete/all list must build cursor continuation into the first Code
|
|
57
57
|
* Mode program; guidance returned after an incomplete outer call is too
|
|
58
58
|
* late to preserve the one-round-trip workflow.
|
|
59
59
|
*
|
|
60
|
-
* v2.103
|
|
60
|
+
* v2.103 - Added the compact Discover -> Select -> Hydrate interpretation
|
|
61
61
|
* fallback for Config-A. Dynamic response orchestration remains
|
|
62
62
|
* authoritative; this sentence only teaches an un-orchestrated client
|
|
63
63
|
* how to act on retrieval envelopes without walking full bodies.
|
|
64
64
|
*
|
|
65
|
-
* v2.102
|
|
65
|
+
* v2.102 - Strengthened the self-bootstrap trigger to a compelling imperative.
|
|
66
66
|
* The v2.101 phrasing ("At the start of a session, unless ... call ...")
|
|
67
67
|
* was too soft; agents on a blind first turn skipped it. Real-world hole
|
|
68
68
|
* it must cover: Cursor's FIRST chat after an IDE open fires no
|
|
69
69
|
* sessionStart (confirmed by hook-stdin capture), and beforeSubmitPrompt
|
|
70
70
|
* has NO model-facing output field (only user_message, shown to the
|
|
71
|
-
* human)
|
|
71
|
+
* human) - so neither the silent digest nor the hook's
|
|
72
72
|
* session_bootstrap_required directive reaches the model. The MCP
|
|
73
73
|
* instruction is the ONLY model-facing channel left for that turn, so it
|
|
74
74
|
* now leads with the blind-state check + a hard "FIRST action MUST be
|
|
75
75
|
* session_bootstrap, before you reply" and an explicit cwd-REQUIRED
|
|
76
76
|
* clause. Still self-scoping: skips when the "Mnemonik project context"
|
|
77
77
|
* block is already present (working hook/proxy delivery), so config-B/C
|
|
78
|
-
* with live delivery don't double-call. cwd stays host-agnostic
|
|
78
|
+
* with live delivery don't double-call. cwd stays host-agnostic - the
|
|
79
79
|
* agent passes the project root it is working in (Cursor exposes the
|
|
80
80
|
* working dir to HOOKS via the stdin `workspace_roots[0]`, which cwdOf
|
|
81
81
|
* already reads; it is NOT an agent-readable env var, so the instruction
|
|
82
82
|
* must not name one).
|
|
83
83
|
*
|
|
84
|
-
* v2.101
|
|
84
|
+
* v2.101 - Stripped to a single instruction: the session_bootstrap trigger.
|
|
85
85
|
* MCP instructions are set at Server construction (initialize), before
|
|
86
|
-
* the server knows the client, so they cannot be scoped per-IDE
|
|
86
|
+
* the server knows the client, so they cannot be scoped per-IDE - and
|
|
87
87
|
* IDE != config (a Claude Code user may be config-B or -C). The content
|
|
88
88
|
* is therefore universal and SELF-SCOPING: it conditions on whether a
|
|
89
89
|
* "Mnemonik project context" block is already present (config-B/C with
|
|
90
|
-
* working delivery
|
|
91
|
-
* delivery missed
|
|
90
|
+
* working delivery -> skip; config-A and any host where hook/proxy
|
|
91
|
+
* delivery missed -> call). Re-enabled in production
|
|
92
92
|
* (MNEMONIK_INSTRUCTIONS_ENABLED=true in both ECS task defs). This is the
|
|
93
|
-
* reliable trigger that makes the agent self-bootstrap
|
|
93
|
+
* reliable trigger that makes the agent self-bootstrap - tool results
|
|
94
94
|
* always reach the model, which hook injection cannot guarantee on a
|
|
95
95
|
* no-tool-call turn (e.g. Cursor "hello").
|
|
96
96
|
*
|
|
97
|
-
* v2.100
|
|
97
|
+
* v2.100 - Skill + IDE rule templates retired entirely. They were the
|
|
98
98
|
* file-written home of the workflow; agents followed them unreliably and
|
|
99
99
|
* forgot them over long sessions (the same fate these MCP instructions
|
|
100
100
|
* suffer), so they are gone. `filesToWrite` now carries only
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
* orchestration layer (host hooks + proxy); these instructions stay lean
|
|
104
104
|
* and are the Config-A fallback only.
|
|
105
105
|
*
|
|
106
|
-
* v2.99
|
|
106
|
+
* v2.99 - Orchestration-aware framing. These static MCP instructions are
|
|
107
107
|
* delivered at `initialize`, before the server knows the actor's tier
|
|
108
108
|
* (no project/proxy/hooks state yet), so they cannot be served per-tier.
|
|
109
109
|
* Instead the CONTENT is now tier-correct: the self-drive calls
|
|
@@ -114,29 +114,29 @@
|
|
|
114
114
|
* The orchestration layer (proxy + host hooks) is authoritative for
|
|
115
115
|
* B/C; this block is primarily Config-A's driver.
|
|
116
116
|
*
|
|
117
|
-
* v2.98
|
|
117
|
+
* v2.98 - Doc truth contracts are the normal drift surface. linkedDocs and
|
|
118
118
|
* doc_code_couplings remain legacy diagnostics only; docs drift
|
|
119
119
|
* defaults to truth findings and legacy:true is explicit debug.
|
|
120
|
-
* v2.97
|
|
120
|
+
* v2.97 - Dropped legacy doc-coupling action guidance. Coupling rows are
|
|
121
121
|
* not truth findings (plan §1).
|
|
122
|
-
* v2.96
|
|
122
|
+
* v2.96 - Replace conditional memory_discover guidance with structural fix:
|
|
123
123
|
* bootstrap now includes _methodCatalog (discoverMemoryTools({})) so
|
|
124
124
|
* agents have the memory_tools calling convention from turn one.
|
|
125
125
|
* Instruction updated to reference _methodCatalog directly.
|
|
126
126
|
* Superseded 2026-07-13: a real Config-B host persisted the expanded
|
|
127
127
|
* 15.1KB hook output instead of injecting it. Bootstrap now carries a
|
|
128
128
|
* compact discovery bridge; memory_discover({}) owns the full catalog.
|
|
129
|
-
* v2.95
|
|
129
|
+
* v2.95 - Drop `augments` from JIT directive verdict list per plan §5 default
|
|
130
130
|
* (augments downgrades to ambient via the parallel recall gate, not
|
|
131
131
|
* the directive lane). See jit-knowledge-injector.md §5 decision note.
|
|
132
|
-
* v2.94
|
|
132
|
+
* v2.94 - Add JIT directive teaching (docs/development/jit-knowledge-injector.md §2.3).
|
|
133
133
|
*
|
|
134
|
-
* Code mode permanent
|
|
134
|
+
* Code mode permanent - all memory operations via memory_tools sandbox.
|
|
135
135
|
* memory_add, file_context etc. are now mnemonik.* methods, not standalone tools.
|
|
136
136
|
*
|
|
137
137
|
* Zero-cooperation rewrite. Context auto-loads if session_bootstrap is skipped.
|
|
138
138
|
* Session summaries are auto-saved if agent doesn't call mnemonik.memory_add().
|
|
139
|
-
* Instructions drastically simplified
|
|
139
|
+
* Instructions drastically simplified - the server handles the workflow now.
|
|
140
140
|
*
|
|
141
141
|
* Token-optimised rewrite (superseded by later instruction rewrites).
|
|
142
142
|
*/
|
package/src/jitDeliveryAck.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureWindowsPrivateDirectorySync } from './runtimeSigners.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import {
|
|
3
4
|
chmodSync,
|
|
@@ -81,6 +82,7 @@ function ensurePrivateDirectory(path: string): boolean {
|
|
|
81
82
|
if (!path) return false;
|
|
82
83
|
try {
|
|
83
84
|
if (existsSync(path) && lstatSync(path).isSymbolicLink()) return false;
|
|
85
|
+
ensureWindowsPrivateDirectorySync(path);
|
|
84
86
|
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
85
87
|
chmodSync(path, 0o700);
|
|
86
88
|
return lstatSync(path).isDirectory() && !lstatSync(path).isSymbolicLink();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { RepositoryFingerprint } from './repositoryFingerprint.js';
|
|
4
|
+
|
|
5
|
+
const CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
6
|
+
export const isCanonicalUuid = (value: unknown): value is string =>
|
|
7
|
+
typeof value === 'string' && CANONICAL_UUID.test(value);
|
|
8
|
+
const IDENTITY_KEYS = new Set([
|
|
9
|
+
'schemaVersion',
|
|
10
|
+
'projectId',
|
|
11
|
+
'projectName',
|
|
12
|
+
'repositoryFingerprint',
|
|
13
|
+
]);
|
|
14
|
+
const FINGERPRINT_KEYS = new Set(['algorithmVersion', 'hash']);
|
|
15
|
+
|
|
16
|
+
export interface ProjectIdentityFile {
|
|
17
|
+
schemaVersion: 1;
|
|
18
|
+
projectId: string;
|
|
19
|
+
projectName?: string;
|
|
20
|
+
repositoryFingerprint?: RepositoryFingerprint;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type IdentityFileResult =
|
|
24
|
+
| { kind: 'ok'; identity: ProjectIdentityFile }
|
|
25
|
+
| { kind: 'unknown_version'; version: unknown }
|
|
26
|
+
| { kind: 'malformed'; detail: string }
|
|
27
|
+
| { kind: 'absent' };
|
|
28
|
+
|
|
29
|
+
export function parseIdentityFile(text: string): Exclude<IdentityFileResult, { kind: 'absent' }> {
|
|
30
|
+
let value: unknown;
|
|
31
|
+
try {
|
|
32
|
+
value = JSON.parse(text);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { kind: 'malformed', detail: `unparseable JSON: ${(error as Error).message}` };
|
|
35
|
+
}
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
return { kind: 'malformed', detail: 'identity file must contain a JSON object' };
|
|
38
|
+
}
|
|
39
|
+
const record = value as Record<string, unknown>;
|
|
40
|
+
if (record.schemaVersion !== 1) {
|
|
41
|
+
return { kind: 'unknown_version', version: record.schemaVersion };
|
|
42
|
+
}
|
|
43
|
+
const unknownKey = Object.keys(record).find((key) => !IDENTITY_KEYS.has(key));
|
|
44
|
+
if (unknownKey) return { kind: 'malformed', detail: `unknown key "${unknownKey}"` };
|
|
45
|
+
if (!isCanonicalUuid(record.projectId)) {
|
|
46
|
+
return { kind: 'malformed', detail: '"projectId" must be a canonical RFC 4122 UUID string' };
|
|
47
|
+
}
|
|
48
|
+
if ('projectName' in record && typeof record.projectName !== 'string') {
|
|
49
|
+
return { kind: 'malformed', detail: '"projectName" must be a string when present' };
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
'repositoryFingerprint' in record &&
|
|
53
|
+
(!record.repositoryFingerprint ||
|
|
54
|
+
typeof record.repositoryFingerprint !== 'object' ||
|
|
55
|
+
Array.isArray(record.repositoryFingerprint))
|
|
56
|
+
) {
|
|
57
|
+
return { kind: 'malformed', detail: '"repositoryFingerprint" must be an object when present' };
|
|
58
|
+
}
|
|
59
|
+
const fingerprint = record.repositoryFingerprint as Record<string, unknown> | undefined;
|
|
60
|
+
const unknownFingerprintKey = fingerprint
|
|
61
|
+
? Object.keys(fingerprint).find((key) => !FINGERPRINT_KEYS.has(key))
|
|
62
|
+
: undefined;
|
|
63
|
+
if (unknownFingerprintKey) {
|
|
64
|
+
return {
|
|
65
|
+
kind: 'malformed',
|
|
66
|
+
detail: `unknown repositoryFingerprint key "${unknownFingerprintKey}"`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (fingerprint && (fingerprint.algorithmVersion !== 1 || typeof fingerprint.hash !== 'string')) {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'malformed',
|
|
72
|
+
detail: '"repositoryFingerprint" must contain algorithmVersion 1 and a string hash',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const repositoryFingerprint: RepositoryFingerprint | undefined = fingerprint
|
|
76
|
+
? { algorithmVersion: 1, hash: fingerprint.hash as string }
|
|
77
|
+
: undefined;
|
|
78
|
+
return {
|
|
79
|
+
kind: 'ok',
|
|
80
|
+
identity: {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
projectId: record.projectId,
|
|
83
|
+
...(record.projectName === undefined ? {} : { projectName: record.projectName as string }),
|
|
84
|
+
...(repositoryFingerprint ? { repositoryFingerprint } : {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function readIdentityFile(dir: string): Promise<IdentityFileResult> {
|
|
90
|
+
const path = join(dir, '.mnemonik.json');
|
|
91
|
+
try {
|
|
92
|
+
const stat = await lstat(path);
|
|
93
|
+
if (stat.isSymbolicLink()) return { kind: 'malformed', detail: 'symlink' };
|
|
94
|
+
if (!stat.isFile()) return { kind: 'malformed', detail: 'identity path is not a regular file' };
|
|
95
|
+
return parseIdentityFile(await readFile(path, 'utf8'));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'absent' };
|
|
98
|
+
return { kind: 'malformed', detail: `cannot read identity file: ${(error as Error).message}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { protectWindowsDirectory } from './runtimeSigners.js';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join, relative } from 'node:path';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { RuntimeReader, hash } from './runtimeReader.js';
|
|
9
|
+
|
|
10
|
+
export const PROJECT_SETUP_MANUAL =
|
|
11
|
+
'Mnemonik project setup needs attention; ask the person to run `mnemonik project init` in this folder.';
|
|
12
|
+
const words = (value: unknown): string =>
|
|
13
|
+
typeof value === 'string' && /^[a-z][a-z0-9_]{0,79}$/.test(value)
|
|
14
|
+
? value.replaceAll('_', ' ')
|
|
15
|
+
: 'needs attention';
|
|
16
|
+
export function projectSetupActions(value: unknown): string {
|
|
17
|
+
return Array.isArray(value) ? value.slice(0, 20).map(words).join(', ') : 'retry, cancel';
|
|
18
|
+
}
|
|
19
|
+
export interface ProjectSetupDiagnostic {
|
|
20
|
+
requestId?: string;
|
|
21
|
+
outcome: 'done' | 'ACTION_REQUIRED' | 'failed';
|
|
22
|
+
time: string;
|
|
23
|
+
rootHash: string | null;
|
|
24
|
+
action: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
28
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
/** Parse the native tool response, never tool arguments or agent-authored command fields. */
|
|
30
|
+
export function setupResponse(value: unknown): Record<string, unknown> | undefined {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
if (value.length > 64 * 1024) return;
|
|
33
|
+
try {
|
|
34
|
+
value = JSON.parse(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!record(value)) return;
|
|
40
|
+
if (record(value.structuredContent)) value = value.structuredContent;
|
|
41
|
+
else {
|
|
42
|
+
// Claude supplies the server's tool_response directly, not model-authored text.
|
|
43
|
+
// Accept only the server envelope; no command/path extensions cross this boundary.
|
|
44
|
+
const keys = ['status', 'state', 'allowedActions', 'requestId', 'expiresAt'];
|
|
45
|
+
const envelope = value;
|
|
46
|
+
if (
|
|
47
|
+
!keys.every((key) => Object.hasOwn(envelope, key)) ||
|
|
48
|
+
Object.keys(value).some((key) => ![...keys, 'manualAction'].includes(key)) ||
|
|
49
|
+
typeof value.state !== 'string' ||
|
|
50
|
+
!Array.isArray(value.allowedActions) ||
|
|
51
|
+
!value.allowedActions.every((action) => typeof action === 'string') ||
|
|
52
|
+
typeof value.requestId !== 'string' ||
|
|
53
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
54
|
+
value.requestId
|
|
55
|
+
) ||
|
|
56
|
+
typeof value.expiresAt !== 'string' ||
|
|
57
|
+
!Number.isFinite(Date.parse(value.expiresAt)) ||
|
|
58
|
+
(Object.hasOwn(value, 'manualAction') && typeof value.manualAction !== 'string')
|
|
59
|
+
)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (record(value) && value.status === 'project_setup_required') return value;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
async function privateDirectory(path: string, session: string): Promise<void> {
|
|
66
|
+
const created = await mkdir(path, { recursive: true, mode: 0o700 });
|
|
67
|
+
const st = await lstat(path);
|
|
68
|
+
if (
|
|
69
|
+
!st.isDirectory() ||
|
|
70
|
+
st.isSymbolicLink() ||
|
|
71
|
+
(process.getuid && (st.uid !== process.getuid() || st.mode & 0o077))
|
|
72
|
+
)
|
|
73
|
+
throw new Error('diagnostic_unavailable');
|
|
74
|
+
if (process.platform === 'win32') {
|
|
75
|
+
await protectWindowsDirectory(path, created !== undefined, undefined, session);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readPrivate(path: string): Promise<string> {
|
|
79
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
80
|
+
try {
|
|
81
|
+
const st = await file.stat();
|
|
82
|
+
if (
|
|
83
|
+
!st.isFile() ||
|
|
84
|
+
st.size > 64 * 1024 ||
|
|
85
|
+
(process.getuid && (st.uid !== process.getuid() || st.mode & 0o077))
|
|
86
|
+
)
|
|
87
|
+
throw new Error('diagnostic_unavailable');
|
|
88
|
+
return await file.readFile('utf8');
|
|
89
|
+
} finally {
|
|
90
|
+
await file.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function writeDiagnostic(path: string, value: ProjectSetupDiagnostic): Promise<void> {
|
|
94
|
+
await privateDirectory(dirname(dirname(path)), dirname(path));
|
|
95
|
+
await privateDirectory(dirname(path), dirname(path));
|
|
96
|
+
const temp = `${path}.${randomUUID()}`;
|
|
97
|
+
try {
|
|
98
|
+
const file = await open(temp, 'wx', 0o600);
|
|
99
|
+
try {
|
|
100
|
+
await file.writeFile(JSON.stringify(value) + '\n');
|
|
101
|
+
} finally {
|
|
102
|
+
await file.close();
|
|
103
|
+
}
|
|
104
|
+
await rename(temp, path);
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(temp, { force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function handoffProjectSetup(input: {
|
|
111
|
+
response: unknown;
|
|
112
|
+
cwd: string | undefined;
|
|
113
|
+
host: 'claude_code' | 'cursor' | 'grok';
|
|
114
|
+
hostSessionId: string;
|
|
115
|
+
stateFile: string;
|
|
116
|
+
stateDir: string;
|
|
117
|
+
familyId: string | undefined;
|
|
118
|
+
}): Promise<string | undefined> {
|
|
119
|
+
const response = setupResponse(input.response);
|
|
120
|
+
if (!response) return;
|
|
121
|
+
let root: string | undefined;
|
|
122
|
+
let action = PROJECT_SETUP_MANUAL;
|
|
123
|
+
let outcome: ProjectSetupDiagnostic['outcome'] = 'failed';
|
|
124
|
+
let reason = 'manual_setup_required';
|
|
125
|
+
const requestId =
|
|
126
|
+
typeof response.requestId === 'string' && response.requestId.length <= 4096
|
|
127
|
+
? response.requestId
|
|
128
|
+
: undefined;
|
|
129
|
+
try {
|
|
130
|
+
if (!input.cwd) throw new Error('safe_cwd_unavailable');
|
|
131
|
+
// The binding receipt is private local state. No path from the response is read.
|
|
132
|
+
const cache = JSON.parse(
|
|
133
|
+
await readPrivate(join(dirname(input.stateFile), 'bound-context.json'))
|
|
134
|
+
);
|
|
135
|
+
const [, family, body] = JSON.parse(cache.key);
|
|
136
|
+
if (
|
|
137
|
+
!input.familyId ||
|
|
138
|
+
family !== input.familyId ||
|
|
139
|
+
body.host !== input.host ||
|
|
140
|
+
body.hostSessionId !== input.hostSessionId ||
|
|
141
|
+
cache.expiresAt <= Date.now() ||
|
|
142
|
+
!cache.canonicalRoot
|
|
143
|
+
)
|
|
144
|
+
throw new Error('bound_context_unavailable');
|
|
145
|
+
root = await realpath(cache.canonicalRoot);
|
|
146
|
+
const { findProjectIdentityDetailed } = await import('./hookRuntime.js');
|
|
147
|
+
const identity = await findProjectIdentityDetailed(await realpath(input.cwd));
|
|
148
|
+
if (identity.kind === 'git_unavailable' || root !== (await realpath(identity.root)))
|
|
149
|
+
throw new Error('bound_root_changed');
|
|
150
|
+
if (!requestId) throw new Error('manual_setup_required');
|
|
151
|
+
const runtime = await new RuntimeReader(input.stateDir).verifyRuntime('cli').catch(() => {
|
|
152
|
+
throw new Error('verified_cli_runtime_unavailable');
|
|
153
|
+
});
|
|
154
|
+
const bin = join(dirname(runtime.entry), 'bin.js');
|
|
155
|
+
if (!runtime.manifest.files[relative(runtime.directory, bin).replaceAll('\\', '/')])
|
|
156
|
+
throw new Error('verified_cli_bin_missing');
|
|
157
|
+
const result = await new Promise<unknown>((resolve, reject) => {
|
|
158
|
+
const child = execFile(
|
|
159
|
+
process.execPath,
|
|
160
|
+
[bin, 'project', 'ensure', '--agent', '--json'],
|
|
161
|
+
{
|
|
162
|
+
cwd: root,
|
|
163
|
+
shell: false,
|
|
164
|
+
timeout: 30_000,
|
|
165
|
+
killSignal: 'SIGKILL',
|
|
166
|
+
maxBuffer: 64 * 1024,
|
|
167
|
+
// Runtime injection variables and repository/PATH executables are not inherited.
|
|
168
|
+
env: Object.fromEntries(
|
|
169
|
+
[
|
|
170
|
+
'HOME',
|
|
171
|
+
'USERPROFILE',
|
|
172
|
+
'LOCALAPPDATA',
|
|
173
|
+
'XDG_STATE_HOME',
|
|
174
|
+
'MNEMONIK_STATE_DIR',
|
|
175
|
+
'MNEMONIK_SERVER',
|
|
176
|
+
'MNEMONIK_API_RESOURCE',
|
|
177
|
+
'MNEMONIK_OAUTH_ISSUER',
|
|
178
|
+
'SYSTEMROOT',
|
|
179
|
+
'TEMP',
|
|
180
|
+
'TMP',
|
|
181
|
+
'TMPDIR',
|
|
182
|
+
].flatMap((key) => (process.env[key] === undefined ? [] : [[key, process.env[key]]]))
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
(error, stdout) => {
|
|
186
|
+
if (error && (error.killed || error.code !== 3))
|
|
187
|
+
return reject(new Error('helper_failed'));
|
|
188
|
+
try {
|
|
189
|
+
resolve(JSON.parse(stdout));
|
|
190
|
+
} catch {
|
|
191
|
+
reject(new Error('helper_invalid_json'));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
child.stdin?.on('error', () => {});
|
|
196
|
+
child.stdin?.end(JSON.stringify({ requestId }));
|
|
197
|
+
});
|
|
198
|
+
if (!record(result)) throw new Error('helper_invalid_json');
|
|
199
|
+
if (result.status === 'done') {
|
|
200
|
+
outcome = 'done';
|
|
201
|
+
reason = 'complete';
|
|
202
|
+
action = 'Mnemonik project setup is complete; call session_bootstrap again.';
|
|
203
|
+
} else if (
|
|
204
|
+
['ACTION_REQUIRED', 'action_required', 'project_setup_required'].includes(
|
|
205
|
+
String(result.status)
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
outcome = 'ACTION_REQUIRED';
|
|
209
|
+
reason = 'choice_required';
|
|
210
|
+
action =
|
|
211
|
+
result.action === 'mnemonik auth renew' ||
|
|
212
|
+
(Array.isArray(result.allowedActions) &&
|
|
213
|
+
result.allowedActions.includes('mnemonik auth renew'))
|
|
214
|
+
? 'mnemonik auth renew'
|
|
215
|
+
: result.allowedActions
|
|
216
|
+
? `Mnemonik project setup needs attention; allowed actions: ${projectSetupActions(result.allowedActions)}.`
|
|
217
|
+
: PROJECT_SETUP_MANUAL;
|
|
218
|
+
} else throw new Error('helper_invalid_json');
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const code = error instanceof Error ? error.message : '';
|
|
221
|
+
reason = /^[a-z_]+$/.test(code) ? code : 'helper_unavailable';
|
|
222
|
+
process.stderr.write(`mnemonik-hook: project setup ${reason}; manual action required\n`);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await writeDiagnostic(input.stateFile, {
|
|
226
|
+
requestId,
|
|
227
|
+
outcome,
|
|
228
|
+
time: new Date().toISOString(),
|
|
229
|
+
rootHash: root ? hash(root) : input.cwd ? hash(await realpath(input.cwd)) : null,
|
|
230
|
+
action,
|
|
231
|
+
reason,
|
|
232
|
+
});
|
|
233
|
+
} catch {
|
|
234
|
+
process.stderr.write('mnemonik-hook: project setup diagnostic unavailable\n');
|
|
235
|
+
}
|
|
236
|
+
return action;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Both doctor and status consume private per-session outcomes, scoped by root hash. */
|
|
240
|
+
export async function pendingProjectSetup(root: string): Promise<ProjectSetupDiagnostic[]> {
|
|
241
|
+
const rootHash = hash(await realpath(root));
|
|
242
|
+
const results: ProjectSetupDiagnostic[] = [];
|
|
243
|
+
const uid = process.getuid?.() ?? 'user';
|
|
244
|
+
for (const host of ['credit', 'cursor', 'grok']) {
|
|
245
|
+
const directory = join(tmpdir(), `mnemonik-${host}-${uid}`);
|
|
246
|
+
try {
|
|
247
|
+
const stat = await lstat(directory);
|
|
248
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
|
|
249
|
+
for (const name of await readdir(directory)) {
|
|
250
|
+
if (!/^[a-f0-9]{64}$/.test(name)) continue;
|
|
251
|
+
try {
|
|
252
|
+
if ((await lstat(join(directory, name))).isSymbolicLink()) continue;
|
|
253
|
+
const diagnostic = JSON.parse(
|
|
254
|
+
await readPrivate(join(directory, name, 'project-setup.json'))
|
|
255
|
+
);
|
|
256
|
+
if (
|
|
257
|
+
(diagnostic.rootHash === rootHash || diagnostic.rootHash === null) &&
|
|
258
|
+
['done', 'ACTION_REQUIRED', 'failed'].includes(diagnostic.outcome) &&
|
|
259
|
+
typeof diagnostic.action === 'string' &&
|
|
260
|
+
diagnostic.action.length < 2000 &&
|
|
261
|
+
Date.now() - Date.parse(diagnostic.time) < 24 * 60 * 60 * 1000
|
|
262
|
+
)
|
|
263
|
+
results.push(diagnostic);
|
|
264
|
+
} catch {
|
|
265
|
+
/* An incomplete or removed session is not a pending action. */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
/* Host has no local sessions. */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// A later completion supersedes stale actions from another session in this root.
|
|
273
|
+
results.sort((a, b) => Date.parse(b.time) - Date.parse(a.time));
|
|
274
|
+
return results[0]?.outcome === 'done' ? [] : results.slice(0, 1);
|
|
275
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { posix, win32, type PlatformPath } from 'node:path';
|
|
3
|
+
|
|
4
|
+
type Platform = NodeJS.Platform;
|
|
5
|
+
|
|
6
|
+
function pathsFor(platform: Platform): PlatformPath {
|
|
7
|
+
return platform === 'win32' ? win32 : posix;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Local state and credential locations that Mnemonik must never scan. */
|
|
11
|
+
export function protectedLocalPaths(
|
|
12
|
+
platform: Platform = process.platform,
|
|
13
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
14
|
+
home: string = homedir()
|
|
15
|
+
): string[] {
|
|
16
|
+
const path = pathsFor(platform);
|
|
17
|
+
const localAppData = env.LOCALAPPDATA || win32.join(home, 'AppData', 'Local');
|
|
18
|
+
const appData = env.APPDATA || win32.join(home, 'AppData', 'Roaming');
|
|
19
|
+
const state =
|
|
20
|
+
env.MNEMONIK_STATE_DIR ||
|
|
21
|
+
(platform === 'win32'
|
|
22
|
+
? win32.join(localAppData, 'Mnemonik')
|
|
23
|
+
: platform === 'darwin'
|
|
24
|
+
? posix.join(home, 'Library', 'Application Support', 'Mnemonik')
|
|
25
|
+
: posix.join(env.XDG_STATE_HOME || posix.join(home, '.local', 'state'), 'mnemonik'));
|
|
26
|
+
const common = [
|
|
27
|
+
state,
|
|
28
|
+
path.join(home, '.mnemonik'),
|
|
29
|
+
path.join(home, '.claude', '.credentials.json'),
|
|
30
|
+
path.join(home, '.codex', 'auth.json'),
|
|
31
|
+
path.join(home, '.cursor', 'mcp.json'),
|
|
32
|
+
path.join(home, '.grok', 'mcp_credentials.json'),
|
|
33
|
+
];
|
|
34
|
+
const osPaths =
|
|
35
|
+
platform === 'darwin'
|
|
36
|
+
? [path.join(home, 'Library', 'Keychains')]
|
|
37
|
+
: platform === 'win32'
|
|
38
|
+
? [
|
|
39
|
+
win32.join(appData, 'Microsoft', 'Credentials'),
|
|
40
|
+
win32.join(localAppData, 'Microsoft', 'Credentials'),
|
|
41
|
+
win32.join(appData, 'Microsoft', 'Protect'),
|
|
42
|
+
]
|
|
43
|
+
: [
|
|
44
|
+
path.join(home, '.local', 'share', 'keyrings'),
|
|
45
|
+
path.join(home, '.gnupg'),
|
|
46
|
+
path.join(home, '.ssh'),
|
|
47
|
+
path.join(home, '.aws'),
|
|
48
|
+
path.join(home, '.config', 'gcloud'),
|
|
49
|
+
path.join(home, '.docker', 'config.json'),
|
|
50
|
+
path.join(home, '.npmrc'),
|
|
51
|
+
path.join(home, '.netrc'),
|
|
52
|
+
path.join(home, '.pypirc'),
|
|
53
|
+
];
|
|
54
|
+
return [...new Set([...common, ...osPaths])];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function comparable(value: string, platform: Platform): string {
|
|
58
|
+
const path = pathsFor(platform);
|
|
59
|
+
const normalized = path.resolve(value);
|
|
60
|
+
return platform === 'win32' || platform === 'darwin' ? normalized.toLowerCase() : normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when candidate is the protected path itself or lies below it. */
|
|
64
|
+
export function isProtectedLocalPath(
|
|
65
|
+
candidate: string,
|
|
66
|
+
protectedPaths: readonly string[] = protectedLocalPaths(),
|
|
67
|
+
platform: Platform = process.platform
|
|
68
|
+
): boolean {
|
|
69
|
+
const path = pathsFor(platform);
|
|
70
|
+
const target = comparable(candidate, platform);
|
|
71
|
+
return protectedPaths.some((protectedPath) => {
|
|
72
|
+
const parent = comparable(protectedPath, platform);
|
|
73
|
+
const relative = path.relative(parent, target);
|
|
74
|
+
return (
|
|
75
|
+
relative === '' ||
|
|
76
|
+
(!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Protected paths equal to or below root, for consent auto-exclusions. */
|
|
82
|
+
export function protectedPathsWithinRoot(
|
|
83
|
+
root: string,
|
|
84
|
+
protectedPaths: readonly string[] = protectedLocalPaths(),
|
|
85
|
+
platform: Platform = process.platform
|
|
86
|
+
): string[] {
|
|
87
|
+
return protectedPaths.filter((candidate) => isProtectedLocalPath(candidate, [root], platform));
|
|
88
|
+
}
|