metheus-governance-mcp-cli 0.2.25 → 0.2.26

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.
Files changed (3) hide show
  1. package/README.md +17 -6
  2. package/cli.mjs +277 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,9 +7,9 @@ Compatibility note: legacy command alias `metheus-governance-mcp` is still suppo
7
7
  - `metheus-governance-mcp-cli` (no args): bootstrap mode
8
8
  - checks auth token
9
9
  - if token is missing/expired, starts `auth login`
10
- - checks Codex/Claude MCP registration
10
+ - checks Codex/Claude/Gemini/Antigravity/Cursor MCP registration
11
11
  - registers only missing clients
12
- - `setup`: register `metheus-governance-mcp` into Codex/Claude (if installed)
12
+ - `setup`: register `metheus-governance-mcp` into Codex/Claude/Gemini/Antigravity/Cursor (if installed)
13
13
  - `doctor`: run end-to-end health checks (auth/registration/gateway/project/ctxpack/tools)
14
14
  - `proxy`: stdio MCP bridge to Metheus HTTPS gateway
15
15
  - `auth`: save/check/clear local Metheus token used by proxy
@@ -40,12 +40,15 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
40
40
  `setup` defaults to `--workspace-dir auto` (dynamic workspace detection).
41
41
  Use an explicit path only when you intentionally want a fixed workspace.
42
42
 
43
- Recommended for Codex/Claude multi-workspace sessions:
43
+ Recommended for Codex/Claude/Gemini/Antigravity/Cursor multi-workspace sessions:
44
44
 
45
45
  ```bash
46
46
  metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com --workspace-dir auto
47
47
  ```
48
48
 
49
+ Gemini CLI note:
50
+ - `gemini mcp` commands require Gemini auth to be configured first (`GEMINI_API_KEY` or `~/.gemini/settings.json` auth).
51
+
49
52
  When a client does not send workspace metadata (for example some Codex sessions),
50
53
  set a stable fallback root once:
51
54
 
@@ -53,7 +56,9 @@ set a stable fallback root once:
53
56
  metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com --workspace-dir auto --workspace-fallback-dir C:\code_test
54
57
  ```
55
58
 
56
- This registers `METHEUS_WORKSPACE_DIR` for Codex MCP so ctxpack sync still resolves safely.
59
+ This sets fallback workspace context for clients that do not pass workspace metadata:
60
+ - Codex/Antigravity/Cursor: `METHEUS_WORKSPACE_DIR` env
61
+ - Gemini: pinned `--workspace-dir <fallback>` in registered MCP command
57
62
 
58
63
  Guardrail note:
59
64
  - By default, CLI blocks reading/writing ctxpack sync metadata when workspace root resolves to the home directory.
@@ -76,7 +81,7 @@ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https:/
76
81
 
77
82
  Checks:
78
83
  - auth token status (+ auto refresh attempt)
79
- - codex/claude registration state
84
+ - codex/claude/gemini/antigravity/cursor registration state
80
85
  - gateway `tools/list` reachability
81
86
  - `project.summary` access
82
87
  - ctxpack auto sync status
@@ -84,7 +89,13 @@ Checks:
84
89
 
85
90
  ## Use in MCP
86
91
 
87
- `setup` auto-registers this server for `codex` and `claude` when those CLIs are available.
92
+ `setup` auto-registers this server for `codex`, `claude`, `gemini`, `antigravity`, and `cursor` when those CLIs are available.
93
+
94
+ Antigravity note:
95
+ - this CLI manages MCP registration via Antigravity local config file (`mcp.json`) because Antigravity does not provide full `mcp list/get/remove` subcommands.
96
+
97
+ Cursor note:
98
+ - this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
88
99
 
89
100
  Local bootstrap tools exposed by proxy:
90
101
 
package/cli.mjs CHANGED
@@ -27,6 +27,7 @@ const SELF_UPDATE_ENV_KEY = "METHEUS_CLI_AUTO_UPDATE";
27
27
  const SELF_UPDATE_GUARD_ENV_KEY = "METHEUS_CLI_SELF_UPDATE_ATTEMPTED";
28
28
  const ALLOW_HOME_WORKSPACE_ENV_KEY = "METHEUS_ALLOW_HOME_WORKSPACE";
29
29
  const AUTO_CTXPACK_SYNC_INTERVAL_MS = 60 * 1000;
30
+ const MCP_CLIENTS = ["codex", "claude", "gemini", "antigravity", "cursor"];
30
31
  const autoCtxpackSyncTracker = new Map();
31
32
 
32
33
  function printUsage() {
@@ -85,6 +86,113 @@ function resolveHomeFilePath(relativePath) {
85
86
  return path.join(home, relativePath);
86
87
  }
87
88
 
89
+ function antigravityMcpConfigFilePath() {
90
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
91
+ if (process.platform === "win32") {
92
+ const appData = String(process.env.APPDATA || "").trim();
93
+ if (appData) {
94
+ return path.join(appData, "Antigravity", "User", "mcp.json");
95
+ }
96
+ if (home) {
97
+ return path.join(home, "AppData", "Roaming", "Antigravity", "User", "mcp.json");
98
+ }
99
+ }
100
+ if (process.platform === "darwin" && home) {
101
+ return path.join(home, "Library", "Application Support", "Antigravity", "User", "mcp.json");
102
+ }
103
+ const xdgConfig = String(process.env.XDG_CONFIG_HOME || "").trim();
104
+ if (xdgConfig) {
105
+ return path.join(xdgConfig, "Antigravity", "User", "mcp.json");
106
+ }
107
+ if (home) {
108
+ return path.join(home, ".config", "Antigravity", "User", "mcp.json");
109
+ }
110
+ return path.resolve(process.cwd(), ".antigravity", "mcp.json");
111
+ }
112
+
113
+ function loadAntigravityMcpConfig() {
114
+ const filePath = antigravityMcpConfigFilePath();
115
+ try {
116
+ const raw = fs.readFileSync(filePath, "utf8");
117
+ const parsed = tryJsonParse(raw);
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
119
+ return { filePath, config: parsed };
120
+ }
121
+ } catch {
122
+ // no-op
123
+ }
124
+ return {
125
+ filePath,
126
+ config: {
127
+ servers: {},
128
+ inputs: [],
129
+ },
130
+ };
131
+ }
132
+
133
+ function saveAntigravityMcpConfig(filePath, config) {
134
+ try {
135
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
136
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ function getAntigravityServerEntry(serverName) {
144
+ const { config } = loadAntigravityMcpConfig();
145
+ const servers = safeObject(config?.servers);
146
+ const entry = safeObject(servers[serverName]);
147
+ if (!entry.command && !entry.url) return null;
148
+ return entry;
149
+ }
150
+
151
+ function cursorMcpConfigFilePath() {
152
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
153
+ if (home) {
154
+ return path.join(home, ".cursor", "mcp.json");
155
+ }
156
+ return path.resolve(process.cwd(), ".cursor", "mcp.json");
157
+ }
158
+
159
+ function loadCursorMcpConfig() {
160
+ const filePath = cursorMcpConfigFilePath();
161
+ try {
162
+ const raw = fs.readFileSync(filePath, "utf8");
163
+ const parsed = tryJsonParse(raw);
164
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
165
+ return { filePath, config: parsed };
166
+ }
167
+ } catch {
168
+ // no-op
169
+ }
170
+ return {
171
+ filePath,
172
+ config: {
173
+ mcpServers: {},
174
+ },
175
+ };
176
+ }
177
+
178
+ function saveCursorMcpConfig(filePath, config) {
179
+ try {
180
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
181
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
182
+ return true;
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ function getCursorServerEntry(serverName) {
189
+ const { config } = loadCursorMcpConfig();
190
+ const servers = safeObject(config?.mcpServers);
191
+ const entry = safeObject(servers[serverName]);
192
+ if (!entry.command && !entry.url) return null;
193
+ return entry;
194
+ }
195
+
88
196
  function updateStateFilePath() {
89
197
  return resolveHomeFilePath(SELF_UPDATE_STATE_RELATIVE_PATH);
90
198
  }
@@ -1392,7 +1500,7 @@ async function runAuthLoginManual(flags) {
1392
1500
  if (exp) {
1393
1501
  process.stdout.write(`Token expires: ${exp}\n`);
1394
1502
  }
1395
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1503
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1396
1504
  }
1397
1505
 
1398
1506
  async function runAuthLoginCallback(flags, baseURL, oidc) {
@@ -1474,7 +1582,7 @@ async function runAuthLoginCallback(flags, baseURL, oidc) {
1474
1582
  process.stdout.write("Refresh token saved.\n");
1475
1583
  }
1476
1584
  process.stdout.write("Auth login complete (callback flow).\n");
1477
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1585
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1478
1586
  }
1479
1587
 
1480
1588
  async function runAuthLoginDevice(flags, baseURL, oidc) {
@@ -1541,7 +1649,7 @@ async function runAuthLoginDevice(flags, baseURL, oidc) {
1541
1649
  process.stdout.write("Refresh token saved.\n");
1542
1650
  }
1543
1651
  process.stdout.write("Auth login complete (device flow).\n");
1544
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1652
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1545
1653
  }
1546
1654
 
1547
1655
  async function runAuthLogin(flags) {
@@ -1825,7 +1933,7 @@ async function runDoctor(flags) {
1825
1933
  );
1826
1934
  }
1827
1935
 
1828
- for (const cliBin of ["codex", "claude"]) {
1936
+ for (const cliBin of MCP_CLIENTS) {
1829
1937
  if (!commandExists(cliBin)) {
1830
1938
  addDoctorCheck(rows, "warn", `${cliBin} CLI`, "not installed; registration check skipped");
1831
1939
  continue;
@@ -4140,7 +4248,7 @@ function runCLICommand(cliBin, args, options = {}) {
4140
4248
  return direct;
4141
4249
  }
4142
4250
  // On Windows, npm global CLIs are often .cmd wrappers.
4143
- // Fallback through cmd.exe so tools like "claude" are discoverable.
4251
+ // Fallback through cmd.exe so tools like "claude"/"gemini"/"antigravity"/"cursor" are discoverable.
4144
4252
  return spawnSync("cmd.exe", ["/d", "/s", "/c", cliBin, ...args], execOptions);
4145
4253
  }
4146
4254
 
@@ -4149,21 +4257,112 @@ function commandExists(bin) {
4149
4257
  return check.status === 0;
4150
4258
  }
4151
4259
 
4260
+ function escapeRegExp(value) {
4261
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4262
+ }
4263
+
4264
+ function hasServerNameInListJSON(value, serverName) {
4265
+ if (Array.isArray(value)) {
4266
+ return value.some((entry) => hasServerNameInListJSON(entry, serverName));
4267
+ }
4268
+ if (!value || typeof value !== "object") return false;
4269
+ if (String(value.name || "").trim() === serverName) return true;
4270
+ return Object.values(value).some((entry) => hasServerNameInListJSON(entry, serverName));
4271
+ }
4272
+
4152
4273
  function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4153
4274
  const selfPath = fileURLToPath(import.meta.url);
4154
- const baseAddArgs =
4155
- cliBin === "claude"
4156
- ? ["mcp", "add", "--scope", "user", serverName]
4157
- : ["mcp", "add", serverName];
4158
4275
  const workspaceEnv = String(options.workspaceDir || "").trim();
4159
- const codexEnvArgs =
4276
+ if (cliBin === "cursor") {
4277
+ const { filePath, config } = loadCursorMcpConfig();
4278
+ const nextConfig = safeObject(config);
4279
+ const nextServers = safeObject(nextConfig.mcpServers);
4280
+ const existing = safeObject(nextServers[serverName]);
4281
+ const args = [selfPath, "proxy", ...proxyArgs];
4282
+ const entry = {
4283
+ ...existing,
4284
+ command: process.execPath,
4285
+ args,
4286
+ };
4287
+ const existingEnv = safeObject(existing.env);
4288
+ if (workspaceEnv) {
4289
+ entry.env = {
4290
+ ...existingEnv,
4291
+ METHEUS_WORKSPACE_DIR: workspaceEnv,
4292
+ };
4293
+ } else if (existingEnv.METHEUS_WORKSPACE_DIR) {
4294
+ const nextEnv = { ...existingEnv };
4295
+ delete nextEnv.METHEUS_WORKSPACE_DIR;
4296
+ if (Object.keys(nextEnv).length > 0) {
4297
+ entry.env = nextEnv;
4298
+ } else {
4299
+ delete entry.env;
4300
+ }
4301
+ }
4302
+ nextServers[serverName] = entry;
4303
+ nextConfig.mcpServers = nextServers;
4304
+ return saveCursorMcpConfig(filePath, nextConfig);
4305
+ }
4306
+ if (cliBin === "antigravity") {
4307
+ const { filePath, config } = loadAntigravityMcpConfig();
4308
+ const nextConfig = safeObject(config);
4309
+ const nextServers = safeObject(nextConfig.servers);
4310
+ const existing = safeObject(nextServers[serverName]);
4311
+ const args = [selfPath, "proxy", ...proxyArgs];
4312
+ const entry = {
4313
+ ...existing,
4314
+ command: process.execPath,
4315
+ args,
4316
+ };
4317
+ const existingEnv = safeObject(existing.env);
4318
+ if (workspaceEnv) {
4319
+ entry.env = {
4320
+ ...existingEnv,
4321
+ METHEUS_WORKSPACE_DIR: workspaceEnv,
4322
+ };
4323
+ } else if (existingEnv.METHEUS_WORKSPACE_DIR) {
4324
+ const nextEnv = { ...existingEnv };
4325
+ delete nextEnv.METHEUS_WORKSPACE_DIR;
4326
+ if (Object.keys(nextEnv).length > 0) {
4327
+ entry.env = nextEnv;
4328
+ } else {
4329
+ delete entry.env;
4330
+ }
4331
+ }
4332
+ nextServers[serverName] = entry;
4333
+ nextConfig.servers = nextServers;
4334
+ if (!Array.isArray(nextConfig.inputs)) {
4335
+ nextConfig.inputs = [];
4336
+ }
4337
+ return saveAntigravityMcpConfig(filePath, nextConfig);
4338
+ }
4339
+ if (cliBin === "gemini") {
4340
+ // Gemini CLI (0.31.x) currently supports:
4341
+ // gemini mcp add <name> <commandOrUrl> [args...]
4342
+ // It does not expose --env for MCP registration, so pin workspace-dir directly.
4343
+ const geminiProxyArgs = workspaceEnv
4344
+ ? withWorkspaceDirArg(proxyArgs, workspaceEnv)
4345
+ : [...proxyArgs];
4346
+ const run = runCLICommand(
4347
+ cliBin,
4348
+ ["mcp", "add", serverName, process.execPath, selfPath, "proxy", ...geminiProxyArgs],
4349
+ { stdio: "inherit" },
4350
+ );
4351
+ return run.status === 0;
4352
+ }
4353
+
4354
+ const baseAddArgs = (() => {
4355
+ if (cliBin === "claude") return ["mcp", "add", "--scope", "user", serverName];
4356
+ return ["mcp", "add", serverName];
4357
+ })();
4358
+ const envArgs =
4160
4359
  cliBin === "codex" && workspaceEnv
4161
4360
  ? ["--env", `METHEUS_WORKSPACE_DIR=${workspaceEnv}`]
4162
4361
  : [];
4163
- if (codexEnvArgs.length > 0) {
4362
+ if (envArgs.length > 0) {
4164
4363
  const envAttempts = [];
4165
- envAttempts.push([...baseAddArgs, ...codexEnvArgs, "--", process.execPath, selfPath, "proxy", ...proxyArgs]);
4166
- envAttempts.push([...baseAddArgs, ...codexEnvArgs, process.execPath, selfPath, "proxy", ...proxyArgs]);
4364
+ envAttempts.push([...baseAddArgs, ...envArgs, "--", process.execPath, selfPath, "proxy", ...proxyArgs]);
4365
+ envAttempts.push([...baseAddArgs, ...envArgs, process.execPath, selfPath, "proxy", ...proxyArgs]);
4167
4366
  for (const args of envAttempts) {
4168
4367
  const run = runCLICommand(cliBin, args, { stdio: "inherit" });
4169
4368
  if (run.status === 0) return true;
@@ -4182,6 +4381,31 @@ function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4182
4381
  }
4183
4382
 
4184
4383
  function runRemove(cliBin, serverName) {
4384
+ if (cliBin === "cursor") {
4385
+ const { filePath, config } = loadCursorMcpConfig();
4386
+ const nextConfig = safeObject(config);
4387
+ const nextServers = safeObject(nextConfig.mcpServers);
4388
+ if (Object.prototype.hasOwnProperty.call(nextServers, serverName)) {
4389
+ delete nextServers[serverName];
4390
+ nextConfig.mcpServers = nextServers;
4391
+ saveCursorMcpConfig(filePath, nextConfig);
4392
+ }
4393
+ return;
4394
+ }
4395
+ if (cliBin === "antigravity") {
4396
+ const { filePath, config } = loadAntigravityMcpConfig();
4397
+ const nextConfig = safeObject(config);
4398
+ const nextServers = safeObject(nextConfig.servers);
4399
+ if (Object.prototype.hasOwnProperty.call(nextServers, serverName)) {
4400
+ delete nextServers[serverName];
4401
+ nextConfig.servers = nextServers;
4402
+ if (!Array.isArray(nextConfig.inputs)) {
4403
+ nextConfig.inputs = [];
4404
+ }
4405
+ saveAntigravityMcpConfig(filePath, nextConfig);
4406
+ }
4407
+ return;
4408
+ }
4185
4409
  if (cliBin === "claude") {
4186
4410
  runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], {
4187
4411
  stdio: "ignore",
@@ -4191,10 +4415,27 @@ function runRemove(cliBin, serverName) {
4191
4415
  });
4192
4416
  return;
4193
4417
  }
4418
+ if (cliBin === "gemini") {
4419
+ runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
4420
+ return;
4421
+ }
4194
4422
  runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
4195
4423
  }
4196
4424
 
4197
4425
  function isRegistered(cliBin, serverName) {
4426
+ if (cliBin === "cursor") {
4427
+ return Boolean(getCursorServerEntry(serverName));
4428
+ }
4429
+ if (cliBin === "antigravity") {
4430
+ return Boolean(getAntigravityServerEntry(serverName));
4431
+ }
4432
+ if (cliBin === "gemini") {
4433
+ const listRun = runCLICommand(cliBin, ["mcp", "list"], { stdio: "pipe" });
4434
+ if (listRun.status !== 0) return false;
4435
+ const text = `${String(listRun.stdout || "")}\n${String(listRun.stderr || "")}`;
4436
+ const pattern = new RegExp(`(^|[\\s"'])${escapeRegExp(serverName)}([\\s"':]|$)`, "m");
4437
+ return pattern.test(text);
4438
+ }
4198
4439
  const args =
4199
4440
  cliBin === "claude"
4200
4441
  ? ["mcp", "get", serverName]
@@ -4204,6 +4445,26 @@ function isRegistered(cliBin, serverName) {
4204
4445
  }
4205
4446
 
4206
4447
  function getRegisteredTransport(cliBin, serverName) {
4448
+ if (cliBin === "cursor") {
4449
+ const entry = getCursorServerEntry(serverName);
4450
+ if (!entry) return null;
4451
+ return {
4452
+ type: "stdio",
4453
+ command: String(entry.command || "").trim(),
4454
+ args: Array.isArray(entry.args) ? entry.args : [],
4455
+ env: safeObject(entry.env),
4456
+ };
4457
+ }
4458
+ if (cliBin === "antigravity") {
4459
+ const entry = getAntigravityServerEntry(serverName);
4460
+ if (!entry) return null;
4461
+ return {
4462
+ type: "stdio",
4463
+ command: String(entry.command || "").trim(),
4464
+ args: Array.isArray(entry.args) ? entry.args : [],
4465
+ env: safeObject(entry.env),
4466
+ };
4467
+ }
4207
4468
  if (cliBin !== "codex") return null;
4208
4469
  const run = runCLICommand(cliBin, ["mcp", "get", serverName, "--json"], { stdio: "pipe" });
4209
4470
  if (run.status !== 0) return null;
@@ -4289,7 +4550,7 @@ function resolveSetupContext(flags) {
4289
4550
  function runSetupInternal(flags, options = {}) {
4290
4551
  const ensureOnly = Boolean(options.ensureOnly);
4291
4552
  const context = resolveSetupContext(flags);
4292
- const clients = ["codex", "claude"];
4553
+ const clients = [...MCP_CLIENTS];
4293
4554
  const results = [];
4294
4555
 
4295
4556
  for (const cliBin of clients) {
@@ -4305,7 +4566,7 @@ function runSetupInternal(flags, options = {}) {
4305
4566
  ? context.workspaceDir
4306
4567
  : context.workspaceFallbackDir;
4307
4568
 
4308
- if (cliBin === "codex" && !context.hasWorkspaceDirFlag && !context.hasWorkspaceFallbackDirFlag) {
4569
+ if ((cliBin === "codex" || cliBin === "cursor" || cliBin === "antigravity") && !context.hasWorkspaceDirFlag && !context.hasWorkspaceFallbackDirFlag) {
4309
4570
  const transport = getRegisteredTransport(cliBin, context.serverName);
4310
4571
  if (transport) {
4311
4572
  const existingWorkspaceDir = extractWorkspaceDirArg(transport.args);
@@ -4348,7 +4609,7 @@ function runSetupInternal(flags, options = {}) {
4348
4609
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
4349
4610
  }
4350
4611
  if (results.length === 0) {
4351
- process.stdout.write("No codex/claude CLI found. Registration skipped.\n");
4612
+ process.stdout.write("No codex/claude/gemini/antigravity/cursor CLI found. Registration skipped.\n");
4352
4613
  return;
4353
4614
  }
4354
4615
  for (const row of results) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [