graph-indexer 3.0.1 → 3.1.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/CHANGELOG.md +456 -0
- package/README.md +61 -8
- package/SECURITY.md +35 -20
- package/bin/graph-indexer.mjs +46 -5
- package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
- package/integrations/claude-code/agents/code-structure.md +0 -3
- package/integrations/claude-code/skills/setup/SKILL.md +26 -0
- package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
- package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
- package/integrations/claude-code-local/README.md +10 -0
- package/integrations/claude-code-local/agents/code-structure.md +22 -0
- package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +15 -4
- package/integrations/opencode/graph-indexer.js +10 -3
- package/package.json +1 -1
- package/src/cli/helper.mjs +15 -1
- package/src/cli/hook.mjs +124 -40
- package/src/cli/init.mjs +32 -5
- package/src/cli/resident.mjs +19 -4
- package/src/index/discover.mjs +52 -30
- package/src/index/indexer.mjs +78 -42
- package/src/index/modules.mjs +75 -2
- package/src/index/parse-worker.mjs +12 -0
- package/src/index/parsepool.mjs +73 -0
- package/src/index/resolver.mjs +362 -23
- package/src/mcp/server.mjs +43 -13
- package/src/mcp/tools.mjs +418 -26
- package/src/parse/arity.mjs +39 -2
- package/src/parse/extract.mjs +269 -45
- package/src/parse/lang/ecma.mjs +2 -0
- package/src/parse/lang/go.mjs +2 -0
- package/src/parse/lang/jvm.mjs +33 -0
- package/src/parse/lang/markdown.mjs +92 -0
- package/src/parse/lang/python.mjs +43 -0
- package/src/parse/lang/rust.mjs +215 -15
- package/src/parse/lang/sfc.mjs +126 -0
- package/src/parse/languages.mjs +9 -2
- package/src/query/check.mjs +49 -13
- package/src/query/facts.mjs +3 -2
- package/src/query/injection-spring.mjs +406 -0
- package/src/query/injection.mjs +174 -0
- package/src/query/intel.mjs +85 -15
- package/src/query/issue.mjs +156 -0
- package/src/query/orm.mjs +288 -0
- package/src/query/read.mjs +6 -1
- package/src/query/report.mjs +53 -0
- package/src/query/routes.mjs +142 -0
- package/src/query/showcase.mjs +59 -0
- package/src/query/textsearch.mjs +30 -9
- package/src/search/search.mjs +85 -17
- package/src/search/semantic.mjs +201 -0
- package/src/search/vectors.mjs +122 -0
- package/src/store/db.mjs +33 -4
- package/src/util/git.mjs +60 -11
- package/src/util/paths.mjs +10 -3
- package/vendor/grammars/README.md +47 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +4 -4
- /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
package/src/store/db.mjs
CHANGED
|
@@ -15,7 +15,7 @@ process.emitWarning = function (warning, ...rest) {
|
|
|
15
15
|
};
|
|
16
16
|
const { DatabaseSync } = await import('node:sqlite');
|
|
17
17
|
|
|
18
|
-
export const SCHEMA_VERSION =
|
|
18
|
+
export const SCHEMA_VERSION = 11;
|
|
19
19
|
|
|
20
20
|
const SCHEMA = `
|
|
21
21
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
@@ -29,7 +29,8 @@ CREATE TABLE IF NOT EXISTS files (
|
|
|
29
29
|
lines INTEGER NOT NULL,
|
|
30
30
|
package TEXT,
|
|
31
31
|
is_test INTEGER NOT NULL DEFAULT 0,
|
|
32
|
-
parse_errors INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
parse_errors INTEGER NOT NULL DEFAULT 0, -- regions the parser could not read (ERROR nodes)
|
|
33
|
+
parse_error_lines TEXT, -- their [start, end] lines as JSON, the first 100
|
|
33
34
|
indexed_at INTEGER NOT NULL
|
|
34
35
|
);
|
|
35
36
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
@@ -69,7 +70,8 @@ CREATE TABLE IF NOT EXISTS refs (
|
|
|
69
70
|
dst_id INTEGER,
|
|
70
71
|
conf REAL NOT NULL DEFAULT 0,
|
|
71
72
|
ncand INTEGER NOT NULL DEFAULT 0,
|
|
72
|
-
argc INTEGER -- arguments of a call or construction (-1: a spread makes it a lower bound), else NULL
|
|
73
|
+
argc INTEGER, -- arguments of a call or construction (-1: a spread makes it a lower bound), else NULL
|
|
74
|
+
arg_types TEXT -- their static types where the language overloads by type (JSON: "int", "String", "?" unknown), else NULL
|
|
73
75
|
);
|
|
74
76
|
CREATE INDEX IF NOT EXISTS refs_file ON refs(file_id);
|
|
75
77
|
CREATE INDEX IF NOT EXISTS refs_dst ON refs(dst_id);
|
|
@@ -102,6 +104,21 @@ CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5(
|
|
|
102
104
|
tokenize = 'porter unicode61 remove_diacritics 2',
|
|
103
105
|
content = '', contentless_delete = 1
|
|
104
106
|
);
|
|
107
|
+
-- semantic channel (optional, src/search/vectors.mjs): one int8 vector per definition, filled lazily
|
|
108
|
+
CREATE TABLE IF NOT EXISTS vectors (
|
|
109
|
+
symbol_id INTEGER PRIMARY KEY,
|
|
110
|
+
file_id INTEGER NOT NULL,
|
|
111
|
+
scale REAL NOT NULL,
|
|
112
|
+
v BLOB NOT NULL
|
|
113
|
+
);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS vectors_file ON vectors(file_id);
|
|
115
|
+
CREATE TABLE IF NOT EXISTS vec_files (file_id INTEGER PRIMARY KEY, hash TEXT NOT NULL);
|
|
116
|
+
-- documentation sections (Markdown headings), apart from code so prose never shifts code's BM25 statistics
|
|
117
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS doc_fts USING fts5(
|
|
118
|
+
name, body,
|
|
119
|
+
tokenize = 'porter unicode61 remove_diacritics 2',
|
|
120
|
+
content = '', contentless_delete = 1
|
|
121
|
+
);
|
|
105
122
|
-- one document per file (rowid = files.id): file-level relevance for hierarchical ranking
|
|
106
123
|
CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
|
|
107
124
|
path, body,
|
|
@@ -111,11 +128,21 @@ CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
|
|
|
111
128
|
`;
|
|
112
129
|
|
|
113
130
|
export class Store {
|
|
131
|
+
/**
|
|
132
|
+
* An index.db the repository shipped is not trusted: defensive mode refuses the statements that
|
|
133
|
+
* can corrupt the file (writable_schema, …) and an untrusted schema may not call functions.
|
|
134
|
+
*/
|
|
135
|
+
#harden() {
|
|
136
|
+
try { this.db.enableDefensive?.(true); } catch { /* older Node */ }
|
|
137
|
+
this.db.exec('PRAGMA trusted_schema = OFF;');
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
constructor(dbPath) {
|
|
115
141
|
this.path = dbPath;
|
|
116
142
|
this.stmts = new Map();
|
|
117
143
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
118
144
|
this.db = new DatabaseSync(dbPath);
|
|
145
|
+
this.#harden();
|
|
119
146
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA temp_store = MEMORY; PRAGMA cache_size = -65536; PRAGMA busy_timeout = 5000;');
|
|
120
147
|
const v = this.#schemaVersion();
|
|
121
148
|
if (v !== null && v !== SCHEMA_VERSION) {
|
|
@@ -123,6 +150,7 @@ export class Store {
|
|
|
123
150
|
this.db.close();
|
|
124
151
|
for (const suffix of ['', '-wal', '-shm']) { try { fs.unlinkSync(dbPath + suffix); } catch { /* none */ } }
|
|
125
152
|
this.db = new DatabaseSync(dbPath);
|
|
153
|
+
this.#harden();
|
|
126
154
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA temp_store = MEMORY; PRAGMA cache_size = -65536; PRAGMA busy_timeout = 5000;');
|
|
127
155
|
}
|
|
128
156
|
this.db.exec(SCHEMA);
|
|
@@ -156,9 +184,10 @@ export class Store {
|
|
|
156
184
|
/** Drop every indexed fact but keep the database (the index is a cache of the source tree). */
|
|
157
185
|
reset() {
|
|
158
186
|
this.tx(() => {
|
|
159
|
-
for (const t of ['files', 'symbols', 'refs', 'imports', 'fields']) this.db.exec(`DELETE FROM ${t}`);
|
|
187
|
+
for (const t of ['files', 'symbols', 'refs', 'imports', 'fields', 'vectors', 'vec_files']) this.db.exec(`DELETE FROM ${t}`);
|
|
160
188
|
this.db.exec("INSERT INTO fts(fts) VALUES ('delete-all')");
|
|
161
189
|
this.db.exec("INSERT INTO file_fts(file_fts) VALUES ('delete-all')");
|
|
190
|
+
this.db.exec("INSERT INTO doc_fts(doc_fts) VALUES ('delete-all')");
|
|
162
191
|
});
|
|
163
192
|
}
|
|
164
193
|
|
package/src/util/git.mjs
CHANGED
|
@@ -1,30 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* git, run so that an indexed repository's own config cannot make it execute anything: no
|
|
3
|
-
* fsmonitor, no hooks, no signature verification, no external diff or textconv,
|
|
4
|
-
* clean/smudge/process filter switched off
|
|
5
|
-
*
|
|
3
|
+
* fsmonitor, no hooks, no signature verification, no external diff or textconv, every
|
|
4
|
+
* clean/smudge/process filter switched off, and no descent into submodules (whose own config the
|
|
5
|
+
* overrides below do not cover). A repository copied with its .git directory (an archive, a shared
|
|
6
|
+
* folder) is not trusted. Every git call graph-indexer makes goes through gitSync.
|
|
6
7
|
*/
|
|
7
8
|
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
8
11
|
|
|
9
12
|
const COMMAND_KEY = /^(filter\.[^\n]+)\.(clean|smudge|process)$|^diff\.[^\n]+\.(textconv|command)$/;
|
|
10
13
|
|
|
14
|
+
let gitBin = null;
|
|
15
|
+
/**
|
|
16
|
+
* The git executable. On Windows a bare `git` is looked up in the working directory before PATH,
|
|
17
|
+
* so a `git.exe` committed at the repository root would run instead: look it up on PATH only.
|
|
18
|
+
*/
|
|
19
|
+
export function gitExecutable() {
|
|
20
|
+
if (gitBin) return gitBin;
|
|
21
|
+
gitBin = 'git';
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
const exts = (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').filter(Boolean);
|
|
24
|
+
for (const dir of (process.env.PATH || process.env.Path || '').split(path.delimiter)) {
|
|
25
|
+
if (!dir || !path.isAbsolute(dir)) continue;
|
|
26
|
+
const hit = exts.map(e => path.join(dir, 'git' + e.toLowerCase())).find(f => { try { return fs.statSync(f).isFile(); } catch { return false; } });
|
|
27
|
+
if (hit) { gitBin = hit; break; }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return gitBin;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let envConfig = null;
|
|
34
|
+
/** git ≥ 2.31 takes config from GIT_CONFIG_COUNT/KEY/VALUE, which carry a key verbatim. */
|
|
35
|
+
function supportsEnvConfig() {
|
|
36
|
+
if (envConfig != null) return envConfig;
|
|
37
|
+
const m = /(\d+)\.(\d+)/.exec(spawnSync(gitExecutable(), ['--version'], { encoding: 'utf8' }).stdout ?? '');
|
|
38
|
+
return (envConfig = !!m && (+m[1] > 2 || (+m[1] === 2 && +m[2] >= 31)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The overrides for `root`: fixed ones, plus one blanking each command its config names. Those go
|
|
43
|
+
* in the environment, not as `-c key=`: git splits a `-c` at the first `=`, and a driver may be
|
|
44
|
+
* named `x=y`, so `-c filter.x=y.clean=` would set `filter.x` and leave the real command on.
|
|
45
|
+
* Returns null when the overrides cannot be expressed (an old git and such a name): git is not run.
|
|
46
|
+
*/
|
|
11
47
|
function hardening(root) {
|
|
12
|
-
const
|
|
13
|
-
const r = spawnSync(
|
|
14
|
-
for (const key of (r.status === 0 ? r.stdout : '').split('\
|
|
48
|
+
const pairs = [['core.fsmonitor', 'false'], ['core.hooksPath', '/dev/null'], ['diff.external', ''], ['log.showSignature', 'false']];
|
|
49
|
+
const r = spawnSync(gitExecutable(), ['config', '-z', '--name-only', '--get-regexp', '^(filter|diff)\\.'], { cwd: root, encoding: 'utf8' });
|
|
50
|
+
for (const key of (r.status === 0 ? r.stdout : '').split('\0')) {
|
|
15
51
|
const m = COMMAND_KEY.exec(key);
|
|
16
52
|
if (!m) continue;
|
|
17
|
-
|
|
53
|
+
pairs.push([key, '']);
|
|
18
54
|
// a required filter (Git LFS sets one) would otherwise fail the diff once its command is blank
|
|
19
|
-
if (m[1])
|
|
55
|
+
if (m[1]) pairs.push([`${m[1]}.required`, 'false']);
|
|
56
|
+
}
|
|
57
|
+
if (supportsEnvConfig()) {
|
|
58
|
+
const base = Number(process.env.GIT_CONFIG_COUNT) || 0;
|
|
59
|
+
const env = { GIT_CONFIG_COUNT: String(base + pairs.length) };
|
|
60
|
+
pairs.forEach(([k, v], i) => { env[`GIT_CONFIG_KEY_${base + i}`] = k; env[`GIT_CONFIG_VALUE_${base + i}`] = v; });
|
|
61
|
+
return { args: [], env };
|
|
20
62
|
}
|
|
21
|
-
return
|
|
63
|
+
if (pairs.some(([k]) => k.includes('='))) return null;
|
|
64
|
+
return { args: pairs.flatMap(([k, v]) => ['-c', `${k}=${v}`]), env: {} };
|
|
22
65
|
}
|
|
23
66
|
|
|
24
|
-
const NO_TOOLS = {
|
|
67
|
+
const NO_TOOLS = {
|
|
68
|
+
diff: ['--no-ext-diff', '--no-textconv', '--ignore-submodules=all'],
|
|
69
|
+
log: ['--no-ext-diff', '--no-textconv', '--no-show-signature'],
|
|
70
|
+
status: ['--ignore-submodules=all'],
|
|
71
|
+
};
|
|
25
72
|
|
|
26
73
|
/** spawnSync('git', args) in `root`, hardened as above; diff and log never run external tools. */
|
|
27
74
|
export function gitSync(root, args, opts = {}) {
|
|
28
75
|
const sub = NO_TOOLS[args[0]] ? [args[0], ...NO_TOOLS[args[0]], ...args.slice(1)] : args;
|
|
29
|
-
|
|
76
|
+
const h = hardening(root);
|
|
77
|
+
if (!h) return { status: 1, stdout: '', stderr: 'git not run: the repository config names a filter or diff driver this git cannot switch off', error: null };
|
|
78
|
+
return spawnSync(gitExecutable(), [...h.args, ...sub], { cwd: root, encoding: 'utf8', ...opts, env: { ...process.env, ...opts.env, ...h.env } });
|
|
30
79
|
}
|
package/src/util/paths.mjs
CHANGED
|
@@ -12,6 +12,11 @@ export function dataDir(root) {
|
|
|
12
12
|
const st = fs.lstatSync(dir);
|
|
13
13
|
if (!st.isDirectory()) throw new Error(`${dir} is not a directory (a symbolic link?); remove it and run again`);
|
|
14
14
|
if ((st.mode & 0o077) !== 0) { try { fs.chmodSync(dir, 0o700); } catch { /* read-only */ } }
|
|
15
|
+
// nor may a link inside it (`index.db-wal` → ~/.zshrc, `resident.started` → ~/.ssh/…) send the
|
|
16
|
+
// index's writes elsewhere: everything in here is ours, so a link is never legitimate
|
|
17
|
+
try {
|
|
18
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) if (e.isSymbolicLink()) fs.unlinkSync(path.join(dir, e.name));
|
|
19
|
+
} catch { /* read-only */ }
|
|
15
20
|
const gi = path.join(dir, '.gitignore');
|
|
16
21
|
if (!fs.existsSync(gi)) { try { fs.writeFileSync(gi, '*\n'); } catch { /* read-only */ } }
|
|
17
22
|
return dir;
|
|
@@ -37,11 +42,13 @@ const realRoots = new Map();
|
|
|
37
42
|
*/
|
|
38
43
|
export function withinRoot(root, rel) {
|
|
39
44
|
let realRoot = realRoots.get(root);
|
|
40
|
-
if (!realRoot) { try { realRoot = fs.realpathSync(root); } catch { realRoot = path.resolve(root); } realRoots.set(root, realRoot); }
|
|
45
|
+
if (!realRoot) { try { realRoot = fs.realpathSync.native(root); } catch { realRoot = path.resolve(root); } realRoots.set(root, realRoot); }
|
|
41
46
|
let real;
|
|
42
|
-
|
|
47
|
+
// the native realpath gives the name as stored on disk, so on a case-insensitive file system
|
|
48
|
+
// (macOS, Windows) `.GIT/config` resolves to `.git/config` and is refused like it
|
|
49
|
+
try { real = fs.realpathSync.native(path.join(root, rel)); } catch { return false; }
|
|
43
50
|
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) return false;
|
|
44
|
-
const top = path.relative(realRoot, real).split(path.sep)[0];
|
|
51
|
+
const top = path.relative(realRoot, real).split(path.sep)[0].toLowerCase();
|
|
45
52
|
return top !== '.git' && top !== DATA_DIR_NAME;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Vendored grammars
|
|
2
|
+
|
|
3
|
+
Each `NAME.wasm.gz` is a tree-sitter grammar compiled to WebAssembly. `manifest.json` records where it
|
|
4
|
+
comes from and the SHA-256 and size of the uncompressed `.wasm`; `test/vendor.test.mjs` checks every
|
|
5
|
+
file against it, and the index fingerprint covers the manifest, so a changed grammar rebuilds indexes.
|
|
6
|
+
|
|
7
|
+
- **`tag` and `asset`**: the `.wasm` published with that release of the grammar's repository,
|
|
8
|
+
downloaded as is.
|
|
9
|
+
- **`commit` and `built`**: the grammar has no release with what graph-indexer needs (Kotlin: the
|
|
10
|
+
latest release, 0.3.8, predates `fun interface` and Kotlin 2.x syntax), so the `.wasm` is built
|
|
11
|
+
from that commit. The build is reproducible: anyone can check the vendored file against its
|
|
12
|
+
source.
|
|
13
|
+
|
|
14
|
+
## Building a grammar from a commit
|
|
15
|
+
|
|
16
|
+
This is `tree-sitter build --wasm` with tree-sitter-cli 0.25.10, the version of the vendored
|
|
17
|
+
`web-tree-sitter`, run as the `emcc` command that CLI issues (`compile_parser_to_wasm` in
|
|
18
|
+
`cli/loader/src/lib.rs`), in the Emscripten image it pins. The checkout must be under a directory
|
|
19
|
+
the container can mount (with Colima, your home directory).
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git clone https://github.com/fwcd/tree-sitter-kotlin && cd tree-sitter-kotlin
|
|
23
|
+
git checkout d6af96d2e23ef0a8ff5ab512864378cf08fba311
|
|
24
|
+
docker run --rm --platform linux/amd64 --workdir /src/src -v "$PWD":/src --user "$(id -u)" \
|
|
25
|
+
emscripten/emsdk:4.0.4 emcc -o output.wasm -Os -s WASM=1 -s SIDE_MODULE=2 -s TOTAL_MEMORY=33554432 \
|
|
26
|
+
-s NODEJS_CATCH_EXIT=0 -s 'EXPORTED_FUNCTIONS=["_tree_sitter_kotlin"]' -fno-exceptions -fvisibility=hidden \
|
|
27
|
+
-I . scanner.c parser.c
|
|
28
|
+
shasum -a 256 src/output.wasm # must equal manifest.json's sha256
|
|
29
|
+
gzip -9 -n -c src/output.wasm > kotlin.wasm.gz
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For another grammar, replace the repository, the commit and the exported symbol
|
|
33
|
+
(`_tree_sitter_<name>`). The grammar's `src/parser.c` must declare `LANGUAGE_VERSION` 14 or 15,
|
|
34
|
+
the ABI versions web-tree-sitter 0.25 loads.
|
|
35
|
+
|
|
36
|
+
## Checking a new grammar before vendoring it
|
|
37
|
+
|
|
38
|
+
A grammar change can move or rename nodes the extraction queries in `src/parse/lang/` match. Before
|
|
39
|
+
replacing one:
|
|
40
|
+
|
|
41
|
+
1. `npm test`, which compiles every query against its grammar.
|
|
42
|
+
2. Parse a corpus of real code in that language with the old and the new grammar and compare the
|
|
43
|
+
files with ERROR regions, the symbols (by kind and qualified name) and the references each
|
|
44
|
+
extracts. A symbol that disappears needs a reason; one that changes its qualified name usually
|
|
45
|
+
moved back into its class.
|
|
46
|
+
3. For a language with a compiler oracle (`bench/eval-graph*.mjs`), the references must keep their
|
|
47
|
+
precision and recall.
|
|
Binary file
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
},
|
|
83
83
|
"kotlin": {
|
|
84
84
|
"repo": "https://github.com/fwcd/tree-sitter-kotlin",
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"sha256": "
|
|
88
|
-
"bytes":
|
|
85
|
+
"commit": "d6af96d2e23ef0a8ff5ab512864378cf08fba311",
|
|
86
|
+
"built": "tree-sitter build --wasm (tree-sitter-cli 0.25.10, emscripten/emsdk:4.0.4): Kotlin 2.x syntax (guard conditions, `..<`, `$$` strings, context parameters) and `fun interface` are in no release yet, the latest being 0.3.8",
|
|
87
|
+
"sha256": "90689092295c4e11d21f2c4f1b9ffce1a3c68f7ce6d4de57c365cd0fc888c7ee",
|
|
88
|
+
"bytes": 6078823,
|
|
89
89
|
"license": "MIT"
|
|
90
90
|
},
|
|
91
91
|
"php": {
|
|
File without changes
|