@viberaven/cli 1.5.1 → 1.5.2

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 (2) hide show
  1. package/dist/cli.js +167 -150
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -14567,7 +14567,7 @@ var VERSION;
14567
14567
  var init_version = __esm({
14568
14568
  "src/version.ts"() {
14569
14569
  "use strict";
14570
- VERSION = "1.5.1";
14570
+ VERSION = "1.5.2";
14571
14571
  }
14572
14572
  });
14573
14573
 
@@ -17266,6 +17266,131 @@ var init_runAudit = __esm({
17266
17266
  }
17267
17267
  });
17268
17268
 
17269
+ // src/commands/cleanPlan.ts
17270
+ function buildContextCleanupPlan(input) {
17271
+ const items = [];
17272
+ const seen = /* @__PURE__ */ new Set();
17273
+ for (const file of input.files) {
17274
+ const path = normalizePath5((0, import_node_path29.relative)(input.projectRoot, file.path) || file.path);
17275
+ const fullPath = normalizePath5(file.path);
17276
+ const category = categorizeCleanupFile(path, fullPath, file);
17277
+ if (!category || seen.has(path)) {
17278
+ continue;
17279
+ }
17280
+ seen.add(path);
17281
+ items.push({
17282
+ path,
17283
+ category,
17284
+ action: "review-ignore-or-exclude-manually",
17285
+ reason: reasonForCategory(category)
17286
+ });
17287
+ }
17288
+ return {
17289
+ title: "VibeRaven context cleanup plan",
17290
+ warning: "VibeRaven only writes this review plan. Review each item manually before changing ignore rules or excluding artifacts from agent context.",
17291
+ items
17292
+ };
17293
+ }
17294
+ async function collectCleanupFiles(projectRoot) {
17295
+ const files = [];
17296
+ await walkCleanupFiles(projectRoot, projectRoot, files, 0);
17297
+ return files;
17298
+ }
17299
+ async function writeCleanupPlan(projectRoot, plan) {
17300
+ const outputDir = (0, import_node_path29.join)(projectRoot, ".viberaven");
17301
+ const outputPath = (0, import_node_path29.join)(outputDir, "context-cleanup.md");
17302
+ await (0, import_promises23.mkdir)(outputDir, { recursive: true });
17303
+ await (0, import_promises23.writeFile)(outputPath, renderCleanupPlan(plan), "utf8");
17304
+ return outputPath;
17305
+ }
17306
+ function categorizeCleanupFile(relativePath, fullPath, file) {
17307
+ if (relativePath.startsWith(".viberaven/") || fullPath.includes("/.viberaven/")) {
17308
+ return "generated-artifact";
17309
+ }
17310
+ if (isCachePath(relativePath)) {
17311
+ return "cache-directory";
17312
+ }
17313
+ if (relativePath.endsWith(".log") && file.sizeBytes >= LARGE_LOG_BYTES) {
17314
+ return "large-log";
17315
+ }
17316
+ return void 0;
17317
+ }
17318
+ function isCachePath(path) {
17319
+ return path === ".next/cache" || path.startsWith(".next/cache/") || path === ".turbo" || path.startsWith(".turbo/") || path === ".vite" || path.startsWith(".vite/") || path.includes("/node_modules/.cache/") || path.endsWith("/node_modules/.cache");
17320
+ }
17321
+ function reasonForCategory(category) {
17322
+ switch (category) {
17323
+ case "generated-artifact":
17324
+ return "Generated VibeRaven artifact. Keep current scan outputs, but avoid loading old reports into agent context.";
17325
+ case "large-log":
17326
+ return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
17327
+ case "cache-directory":
17328
+ return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
17329
+ }
17330
+ }
17331
+ function renderCleanupPlan(plan) {
17332
+ const lines = ["# VibeRaven context cleanup plan", "", plan.warning, ""];
17333
+ if (plan.items.length === 0) {
17334
+ lines.push("No noisy generated artifacts, large logs, or cache directories were found by the conservative scanner.");
17335
+ }
17336
+ for (const item4 of plan.items) {
17337
+ lines.push(`- ${item4.path}`);
17338
+ lines.push(` - Category: ${item4.category}`);
17339
+ lines.push(` - Action: ${item4.action}`);
17340
+ lines.push(` - Reason: ${item4.reason}`);
17341
+ }
17342
+ return `${lines.join("\n")}
17343
+ `;
17344
+ }
17345
+ async function walkCleanupFiles(root, dir, result, depth) {
17346
+ if (depth > MAX_WALK_DEPTH) {
17347
+ return;
17348
+ }
17349
+ let entries;
17350
+ try {
17351
+ entries = await (0, import_promises23.readdir)(dir, { withFileTypes: true });
17352
+ } catch {
17353
+ return;
17354
+ }
17355
+ for (const entry of entries) {
17356
+ if (SKIP_DIRECTORIES.has(entry.name)) {
17357
+ continue;
17358
+ }
17359
+ const absolute = (0, import_node_path29.join)(dir, entry.name);
17360
+ if (entry.isDirectory()) {
17361
+ if (isCachePath(normalizePath5((0, import_node_path29.relative)(root, absolute)))) {
17362
+ result.push({ path: absolute, sizeBytes: 0, kind: "directory" });
17363
+ continue;
17364
+ }
17365
+ await walkCleanupFiles(root, absolute, result, depth + 1);
17366
+ continue;
17367
+ }
17368
+ if (!entry.isFile()) {
17369
+ continue;
17370
+ }
17371
+ try {
17372
+ const fileStat = await (0, import_promises23.stat)(absolute);
17373
+ result.push({ path: absolute, sizeBytes: fileStat.size, kind: "file" });
17374
+ } catch {
17375
+ continue;
17376
+ }
17377
+ }
17378
+ }
17379
+ function normalizePath5(path) {
17380
+ return path.replace(/\\/g, "/");
17381
+ }
17382
+ var import_promises23, import_node_path29, MAX_WALK_DEPTH, LARGE_LOG_BYTES, SKIP_DIRECTORIES;
17383
+ var init_cleanPlan = __esm({
17384
+ "src/commands/cleanPlan.ts"() {
17385
+ "use strict";
17386
+ import_promises23 = require("node:fs/promises");
17387
+ import_node_path29 = require("node:path");
17388
+ MAX_WALK_DEPTH = 5;
17389
+ LARGE_LOG_BYTES = 8192;
17390
+ SKIP_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
17391
+ }
17392
+ });
17393
+
17269
17394
  // src/actions/render.ts
17270
17395
  function redact(text) {
17271
17396
  return redactString(text).replace(/\[REDACTED(?:_[A-Z_]+)?\]/g, "<redacted>").replace(
@@ -17368,10 +17493,10 @@ var init_render = __esm({
17368
17493
 
17369
17494
  // src/commands/actions.ts
17370
17495
  async function runActionsCommand(input) {
17371
- const actionsPath = (0, import_node_path29.join)(getProjectArtifactsDir(input.cwd), "actions.json");
17496
+ const actionsPath = (0, import_node_path30.join)(getProjectArtifactsDir(input.cwd), "actions.json");
17372
17497
  let raw;
17373
17498
  try {
17374
- raw = await (0, import_promises23.readFile)(actionsPath, "utf8");
17499
+ raw = await (0, import_promises24.readFile)(actionsPath, "utf8");
17375
17500
  } catch {
17376
17501
  process.stderr.write("No VibeRaven actions found. Run `npx -y viberaven --agent-mode` first.\n");
17377
17502
  return 1;
@@ -17385,12 +17510,12 @@ async function runActionsCommand(input) {
17385
17510
  process.stdout.write(renderActionSurface(manifest));
17386
17511
  return 0;
17387
17512
  }
17388
- var import_promises23, import_node_path29;
17513
+ var import_promises24, import_node_path30;
17389
17514
  var init_actions = __esm({
17390
17515
  "src/commands/actions.ts"() {
17391
17516
  "use strict";
17392
- import_promises23 = require("node:fs/promises");
17393
- import_node_path29 = require("node:path");
17517
+ import_promises24 = require("node:fs/promises");
17518
+ import_node_path30 = require("node:path");
17394
17519
  init_render();
17395
17520
  init_config();
17396
17521
  }
@@ -17399,7 +17524,7 @@ var init_actions = __esm({
17399
17524
  // src/commands/verifyAction.ts
17400
17525
  async function currentActionExists(cwd, actionId) {
17401
17526
  try {
17402
- const raw = await (0, import_promises24.readFile)((0, import_node_path30.join)(getProjectArtifactsDir(cwd), "actions.json"), "utf8");
17527
+ const raw = await (0, import_promises25.readFile)((0, import_node_path31.join)(getProjectArtifactsDir(cwd), "actions.json"), "utf8");
17403
17528
  const manifest = JSON.parse(raw);
17404
17529
  return manifest.actions.some((action) => action.id === actionId);
17405
17530
  } catch {
@@ -17443,12 +17568,12 @@ Run \`npx -y viberaven --verify\` to refresh the production gate for this action
17443
17568
  );
17444
17569
  return 0;
17445
17570
  }
17446
- var import_promises24, import_node_path30;
17571
+ var import_promises25, import_node_path31;
17447
17572
  var init_verifyAction = __esm({
17448
17573
  "src/commands/verifyAction.ts"() {
17449
17574
  "use strict";
17450
- import_promises24 = require("node:fs/promises");
17451
- import_node_path30 = require("node:path");
17575
+ import_promises25 = require("node:fs/promises");
17576
+ import_node_path31 = require("node:path");
17452
17577
  init_registry2();
17453
17578
  init_config();
17454
17579
  }
@@ -18076,8 +18201,8 @@ function isSqlError(error) {
18076
18201
  }
18077
18202
  function displayPath(projectDir, path) {
18078
18203
  for (const base of [projectDir, process.cwd()]) {
18079
- const rel = (0, import_node_path31.relative)(base, path);
18080
- if (rel && !rel.startsWith("..") && !(0, import_node_path31.isAbsolute)(rel)) return rel.split("\\").join("/");
18204
+ const rel = (0, import_node_path32.relative)(base, path);
18205
+ if (rel && !rel.startsWith("..") && !(0, import_node_path32.isAbsolute)(rel)) return rel.split("\\").join("/");
18081
18206
  }
18082
18207
  return path;
18083
18208
  }
@@ -18681,26 +18806,26 @@ function verdictFor(summary) {
18681
18806
  }
18682
18807
  async function listMigrations(dir) {
18683
18808
  try {
18684
- const entries = await (0, import_promises25.readdir)(dir, { withFileTypes: true });
18809
+ const entries = await (0, import_promises26.readdir)(dir, { withFileTypes: true });
18685
18810
  return entries.filter((e2) => e2.isFile() && /\.sql$/i.test(e2.name) && !e2.name.startsWith(".")).map((e2) => e2.name).sort((a, b3) => a < b3 ? -1 : a > b3 ? 1 : 0);
18686
18811
  } catch {
18687
18812
  return [];
18688
18813
  }
18689
18814
  }
18690
18815
  async function runRlsTest(options) {
18691
- const projectDir = (0, import_node_path31.resolve)(options.projectDir);
18692
- const matrixPath = (0, import_node_path31.resolve)(projectDir, options.matrixPath ?? DEFAULT_MATRIX_FILE);
18693
- const migrationsDir = (0, import_node_path31.resolve)(projectDir, options.migrationsDir ?? (0, import_node_path31.join)("supabase", "migrations"));
18816
+ const projectDir = (0, import_node_path32.resolve)(options.projectDir);
18817
+ const matrixPath = (0, import_node_path32.resolve)(projectDir, options.matrixPath ?? DEFAULT_MATRIX_FILE);
18818
+ const migrationsDir = (0, import_node_path32.resolve)(projectDir, options.migrationsDir ?? (0, import_node_path32.join)("supabase", "migrations"));
18694
18819
  const timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
18695
- if ((0, import_node_path31.basename)(matrixPath).toLowerCase().startsWith(".env")) {
18696
- throw new RlsInputError(`Refusing to read ${(0, import_node_path31.basename)(matrixPath)}: rls-test never reads .env files. Point --matrix at a viberaven.permissions.json.`);
18820
+ if ((0, import_node_path32.basename)(matrixPath).toLowerCase().startsWith(".env")) {
18821
+ throw new RlsInputError(`Refusing to read ${(0, import_node_path32.basename)(matrixPath)}: rls-test never reads .env files. Point --matrix at a viberaven.permissions.json.`);
18697
18822
  }
18698
18823
  const filesRead = [];
18699
18824
  let matrixText;
18700
18825
  try {
18701
- const info = await (0, import_promises25.stat)(matrixPath);
18826
+ const info = await (0, import_promises26.stat)(matrixPath);
18702
18827
  if (!info.isFile()) throw new Error("not a file");
18703
- matrixText = normalise(await (0, import_promises25.readFile)(matrixPath, "utf8"));
18828
+ matrixText = normalise(await (0, import_promises26.readFile)(matrixPath, "utf8"));
18704
18829
  } catch {
18705
18830
  throw new RlsInputError(
18706
18831
  `No permissions matrix at ${matrixPath}. Write one by hand (schema: schemas/permissions-matrix.schema.json in the @viberaven/cli package); rls-test never infers it from your policies.`
@@ -18711,8 +18836,8 @@ async function runRlsTest(options) {
18711
18836
  const names = await listMigrations(migrationsDir);
18712
18837
  const migrationFiles = [];
18713
18838
  for (const name of names) {
18714
- const sql = normalise(await (0, import_promises25.readFile)((0, import_node_path31.join)(migrationsDir, name), "utf8"));
18715
- filesRead.push(displayPath(projectDir, (0, import_node_path31.join)(migrationsDir, name)));
18839
+ const sql = normalise(await (0, import_promises26.readFile)((0, import_node_path32.join)(migrationsDir, name), "utf8"));
18840
+ filesRead.push(displayPath(projectDir, (0, import_node_path32.join)(migrationsDir, name)));
18716
18841
  migrationFiles.push({ name, sha256: sha256(sql), sql });
18717
18842
  }
18718
18843
  const migrationsHash = sha256(migrationFiles.map((f) => `${f.name}\0${f.sha256}`).join("\n"));
@@ -18912,15 +19037,15 @@ async function runTable(db, table, badRoles, roleChecks) {
18912
19037
  cells
18913
19038
  };
18914
19039
  }
18915
- var import_node_child_process6, import_node_crypto2, import_promises25, import_node_net, import_node_path31, import_node_util4, NON_AUTHORITATIVE_LABEL, PGLITE_INSTALL_HINT, ENV_ALLOWLIST, PERMISSION_DENIED, RLS_REFUSAL, DEFAULT_TIMEOUTS, RlsInputError, normalise, quoteIdent2, firstLine, plural2, NUMBERS, ACTOR_SESSION, changedRows, isRlsRefusal, isPrivilegeRefusal, SELF, PROBE_EXCLUSIONS, SEED_HINTS;
19040
+ var import_node_child_process6, import_node_crypto2, import_promises26, import_node_net, import_node_path32, import_node_util4, NON_AUTHORITATIVE_LABEL, PGLITE_INSTALL_HINT, ENV_ALLOWLIST, PERMISSION_DENIED, RLS_REFUSAL, DEFAULT_TIMEOUTS, RlsInputError, normalise, quoteIdent2, firstLine, plural2, NUMBERS, ACTOR_SESSION, changedRows, isRlsRefusal, isPrivilegeRefusal, SELF, PROBE_EXCLUSIONS, SEED_HINTS;
18916
19041
  var init_rlsRunner = __esm({
18917
19042
  "src/rls/rlsRunner.ts"() {
18918
19043
  "use strict";
18919
19044
  import_node_child_process6 = require("node:child_process");
18920
19045
  import_node_crypto2 = require("node:crypto");
18921
- import_promises25 = require("node:fs/promises");
19046
+ import_promises26 = require("node:fs/promises");
18922
19047
  import_node_net = __toESM(require("node:net"));
18923
- import_node_path31 = require("node:path");
19048
+ import_node_path32 = require("node:path");
18924
19049
  import_node_util4 = require("node:util");
18925
19050
  init_safeGit();
18926
19051
  init_permissionsMatrix();
@@ -19085,131 +19210,6 @@ var init_rlsTest = __esm({
19085
19210
  }
19086
19211
  });
19087
19212
 
19088
- // src/commands/cleanPlan.ts
19089
- function buildContextCleanupPlan(input) {
19090
- const items = [];
19091
- const seen = /* @__PURE__ */ new Set();
19092
- for (const file of input.files) {
19093
- const path = normalizePath5((0, import_node_path32.relative)(input.projectRoot, file.path) || file.path);
19094
- const fullPath = normalizePath5(file.path);
19095
- const category = categorizeCleanupFile(path, fullPath, file);
19096
- if (!category || seen.has(path)) {
19097
- continue;
19098
- }
19099
- seen.add(path);
19100
- items.push({
19101
- path,
19102
- category,
19103
- action: "review-ignore-or-exclude-manually",
19104
- reason: reasonForCategory(category)
19105
- });
19106
- }
19107
- return {
19108
- title: "VibeRaven context cleanup plan",
19109
- warning: "VibeRaven only writes this review plan. Review each item manually before changing ignore rules or excluding artifacts from agent context.",
19110
- items
19111
- };
19112
- }
19113
- async function collectCleanupFiles(projectRoot) {
19114
- const files = [];
19115
- await walkCleanupFiles(projectRoot, projectRoot, files, 0);
19116
- return files;
19117
- }
19118
- async function writeCleanupPlan(projectRoot, plan) {
19119
- const outputDir = (0, import_node_path32.join)(projectRoot, ".viberaven");
19120
- const outputPath = (0, import_node_path32.join)(outputDir, "context-cleanup.md");
19121
- await (0, import_promises26.mkdir)(outputDir, { recursive: true });
19122
- await (0, import_promises26.writeFile)(outputPath, renderCleanupPlan(plan), "utf8");
19123
- return outputPath;
19124
- }
19125
- function categorizeCleanupFile(relativePath, fullPath, file) {
19126
- if (relativePath.startsWith(".viberaven/") || fullPath.includes("/.viberaven/")) {
19127
- return "generated-artifact";
19128
- }
19129
- if (isCachePath(relativePath)) {
19130
- return "cache-directory";
19131
- }
19132
- if (relativePath.endsWith(".log") && file.sizeBytes >= LARGE_LOG_BYTES) {
19133
- return "large-log";
19134
- }
19135
- return void 0;
19136
- }
19137
- function isCachePath(path) {
19138
- return path === ".next/cache" || path.startsWith(".next/cache/") || path === ".turbo" || path.startsWith(".turbo/") || path === ".vite" || path.startsWith(".vite/") || path.includes("/node_modules/.cache/") || path.endsWith("/node_modules/.cache");
19139
- }
19140
- function reasonForCategory(category) {
19141
- switch (category) {
19142
- case "generated-artifact":
19143
- return "Generated VibeRaven artifact. Keep current scan outputs, but avoid loading old reports into agent context.";
19144
- case "large-log":
19145
- return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
19146
- case "cache-directory":
19147
- return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
19148
- }
19149
- }
19150
- function renderCleanupPlan(plan) {
19151
- const lines = ["# VibeRaven context cleanup plan", "", plan.warning, ""];
19152
- if (plan.items.length === 0) {
19153
- lines.push("No noisy generated artifacts, large logs, or cache directories were found by the conservative scanner.");
19154
- }
19155
- for (const item4 of plan.items) {
19156
- lines.push(`- ${item4.path}`);
19157
- lines.push(` - Category: ${item4.category}`);
19158
- lines.push(` - Action: ${item4.action}`);
19159
- lines.push(` - Reason: ${item4.reason}`);
19160
- }
19161
- return `${lines.join("\n")}
19162
- `;
19163
- }
19164
- async function walkCleanupFiles(root, dir, result, depth) {
19165
- if (depth > MAX_WALK_DEPTH) {
19166
- return;
19167
- }
19168
- let entries;
19169
- try {
19170
- entries = await (0, import_promises26.readdir)(dir, { withFileTypes: true });
19171
- } catch {
19172
- return;
19173
- }
19174
- for (const entry of entries) {
19175
- if (SKIP_DIRECTORIES.has(entry.name)) {
19176
- continue;
19177
- }
19178
- const absolute = (0, import_node_path32.join)(dir, entry.name);
19179
- if (entry.isDirectory()) {
19180
- if (isCachePath(normalizePath5((0, import_node_path32.relative)(root, absolute)))) {
19181
- result.push({ path: absolute, sizeBytes: 0, kind: "directory" });
19182
- continue;
19183
- }
19184
- await walkCleanupFiles(root, absolute, result, depth + 1);
19185
- continue;
19186
- }
19187
- if (!entry.isFile()) {
19188
- continue;
19189
- }
19190
- try {
19191
- const fileStat = await (0, import_promises26.stat)(absolute);
19192
- result.push({ path: absolute, sizeBytes: fileStat.size, kind: "file" });
19193
- } catch {
19194
- continue;
19195
- }
19196
- }
19197
- }
19198
- function normalizePath5(path) {
19199
- return path.replace(/\\/g, "/");
19200
- }
19201
- var import_promises26, import_node_path32, MAX_WALK_DEPTH, LARGE_LOG_BYTES, SKIP_DIRECTORIES;
19202
- var init_cleanPlan = __esm({
19203
- "src/commands/cleanPlan.ts"() {
19204
- "use strict";
19205
- import_promises26 = require("node:fs/promises");
19206
- import_node_path32 = require("node:path");
19207
- MAX_WALK_DEPTH = 5;
19208
- LARGE_LOG_BYTES = 8192;
19209
- SKIP_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
19210
- }
19211
- });
19212
-
19213
19213
  // src/report/providerLogos.ts
19214
19214
  function providerAssetUrl(key) {
19215
19215
  const file = PROVIDER_ASSET_FILES[key];
@@ -24219,6 +24219,9 @@ Usage:
24219
24219
  viberaven audit --vercel-supabase [--json] [path]
24220
24220
  Local Vercel/Supabase repo evidence audit (RLS, pooler, secrets)
24221
24221
 
24222
+ viberaven clean --plan [path]
24223
+ Write a non-destructive context cleanup review plan (.viberaven/context-cleanup.md)
24224
+
24222
24225
  viberaven rls-test [--matrix viberaven.permissions.json] [--migrations dir] [--json] [path]
24223
24226
  Experimental: run the Supabase migrations in a PGlite imitation and test who can
24224
24227
  read, change and delete rows against your permissions matrix (non-authoritative)
@@ -24738,6 +24741,19 @@ async function main() {
24738
24741
  cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
24739
24742
  json: Boolean(flags.json)
24740
24743
  });
24744
+ case "clean": {
24745
+ if (flags.plan !== true) {
24746
+ console.error("Usage: viberaven clean --plan [path]");
24747
+ return 1;
24748
+ }
24749
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
24750
+ const plan = buildContextCleanupPlan({ projectRoot: cwd, files: await collectCleanupFiles(cwd) });
24751
+ const outputPath = await writeCleanupPlan(cwd, plan);
24752
+ console.log(`Wrote ${outputPath}
24753
+ `);
24754
+ console.log(renderCleanupPlan(plan).trimEnd());
24755
+ return 0;
24756
+ }
24741
24757
  case "rls-test":
24742
24758
  return runRlsTestCommand({
24743
24759
  cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
@@ -24782,6 +24798,7 @@ var init_cli = __esm({
24782
24798
  init_runDoctor();
24783
24799
  init_runValidateNpmPackage();
24784
24800
  init_runAudit();
24801
+ init_cleanPlan();
24785
24802
  init_actions();
24786
24803
  init_verifyAction();
24787
24804
  init_preview();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viberaven/cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Local Studio cockpit for AI-built apps: agentic chat, provider context, release review, diffs, and approval-controlled repo work.",
5
5
  "license": "MIT",
6
6
  "author": "VibeRaven",