@akasecurity/cli 0.9.2 → 0.9.3

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 (86) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +258 -78
  3. package/package.json +4 -4
  4. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  5. package/web-ui/web-ui/.next/app-build-manifest.json +39 -39
  6. package/web-ui/web-ui/.next/app-path-routes-manifest.json +5 -5
  7. package/web-ui/web-ui/.next/build-manifest.json +2 -2
  8. package/web-ui/web-ui/.next/prerender-manifest.json +18 -18
  9. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  10. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  11. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  12. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  13. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  14. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  15. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  16. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  17. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +1 -1
  18. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  19. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +1 -1
  20. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  21. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +1 -1
  22. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  23. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  24. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  25. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  26. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  28. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  29. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  31. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +1 -1
  32. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  34. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  35. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  36. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  37. package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
  38. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  39. package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
  40. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/web-ui/web-ui/.next/server/app-paths-manifest.json +5 -5
  42. package/web-ui/web-ui/.next/server/chunks/142.js +7 -7
  43. package/web-ui/web-ui/.next/server/chunks/472.js +2 -2
  44. package/web-ui/web-ui/.next/server/chunks/830.js +2 -2
  45. package/web-ui/web-ui/.next/server/chunks/853.js +5 -5
  46. package/web-ui/web-ui/.next/server/chunks/871.js +1 -1
  47. package/web-ui/web-ui/.next/server/functions-config-manifest.json +4 -4
  48. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  49. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  50. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  51. package/web-ui/web-ui/.next/server/pages-manifest.json +1 -1
  52. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  53. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  54. package/web-ui/web-ui/.next/static/chunks/517-a4643cfd51efbca4.js +1 -0
  55. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-72558bcb945a0abc.js +1 -0
  56. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-540aa3dbe1c0b3ad.js +1 -0
  57. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8fac90d5b77b7f89.js +1 -0
  58. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/{page-3a5138f424e43b6e.js → page-06ee1868466199df.js} +1 -1
  59. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-8b2ea0bbbc4fc69c.js +1 -0
  60. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-2aa604a6c36786fc.js +1 -0
  61. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-28f48db02051d88c.js +1 -0
  62. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-15bc43471843cf2e.js +1 -0
  63. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-f034effcd10a332b.js +1 -0
  64. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-de465eacbb359987.js +1 -0
  65. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-fa10c50b1436102d.js +1 -0
  66. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-a61b90eb3a575f85.js +1 -0
  67. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-be0284caa1e78227.js +1 -0
  68. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-eec4e54274dcbb4d.js +1 -0
  69. package/web-ui/web-ui/.next/static/css/a30725ee12aca680.css +2 -0
  70. package/web-ui/web-ui/.next/static/chunks/517-977bc0a7a7be88cc.js +0 -1
  71. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-96995843d6cbfb84.js +0 -1
  72. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-28eeea5c4a816f53.js +0 -1
  73. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-0c3888b9381a6e80.js +0 -1
  74. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-32d67b6c8165acda.js +0 -1
  75. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1c376f3818bfba45.js +0 -1
  76. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-9c9b1d29895a0592.js +0 -1
  77. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-4334a50ed9dcace4.js +0 -1
  78. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-de6da4ea92784950.js +0 -1
  79. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-4817c44647618832.js +0 -1
  80. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-5d2aa8c946dd2cae.js +0 -1
  81. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-c0597d1c1acb1cc4.js +0 -1
  82. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-2a7b0d9dab536ae1.js +0 -1
  83. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-576a976afbe33369.js +0 -1
  84. package/web-ui/web-ui/.next/static/css/1ea302d29fc67ba4.css +0 -2
  85. /package/web-ui/web-ui/.next/static/{mAOCejrDqkMoDfzjWr6qu → X_Z1LZQpnGXTuZXKkv4Vb}/_buildManifest.js +0 -0
  86. /package/web-ui/web-ui/.next/static/{mAOCejrDqkMoDfzjWr6qu → X_Z1LZQpnGXTuZXKkv4Vb}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -48,6 +48,10 @@ aka dashboard # open the local web dashboard over your store
48
48
 
49
49
  Run `aka --help` for the full command list.
50
50
 
51
+ ### External subcommands
52
+
53
+ Any other command dispatches, git-style, to an executable named `aka-<command>` on your PATH (POSIX only; built-ins always win). For example, with [aka-claude-tools](https://github.com/akasecurity/claude-tools) installed, `aka claude` launches the hardened Claude Code profile via its `aka-claude` launcher.
54
+
51
55
  ## The Claude Code plugin
52
56
 
53
57
  The CLI gives you the dashboard, store, and scanning. To actually **intercept** a Claude Code session you also need the AKA plugin, which installs from the Claude Code plugin marketplace (not npm). With the CLI installed:
package/dist/cli.js CHANGED
@@ -18381,6 +18381,9 @@ var ExceptionBundleEntry = DetectionException.pick({
18381
18381
  useCount: true,
18382
18382
  conditions: true
18383
18383
  });
18384
+ function isMatchableUnder(keyVersion, key) {
18385
+ return key.status === "present" && key.version === keyVersion;
18386
+ }
18384
18387
 
18385
18388
  // ../packages/schema/src/zod/rule.ts
18386
18389
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -25500,11 +25503,22 @@ function purgeSampleData(db) {
25500
25503
  function linkHost(input, hostId) {
25501
25504
  return hostId ? { ...input, hostId } : input;
25502
25505
  }
25506
+ function closeQuietly(db) {
25507
+ try {
25508
+ db.close();
25509
+ } catch {
25510
+ }
25511
+ }
25503
25512
  function openWithPragmas(file2) {
25504
25513
  const db = new DatabaseSync(file2);
25505
- db.exec("PRAGMA journal_mode = WAL");
25506
- db.exec("PRAGMA busy_timeout = 2000");
25507
- db.exec("PRAGMA foreign_keys = ON");
25514
+ try {
25515
+ db.exec("PRAGMA journal_mode = WAL");
25516
+ db.exec("PRAGMA busy_timeout = 2000");
25517
+ db.exec("PRAGMA foreign_keys = ON");
25518
+ } catch (err) {
25519
+ closeQuietly(db);
25520
+ throw err;
25521
+ }
25508
25522
  return db;
25509
25523
  }
25510
25524
  function backupLegacyStore(file2) {
@@ -25516,43 +25530,80 @@ function backupLegacyStore(file2) {
25516
25530
  }
25517
25531
  return backup;
25518
25532
  }
25533
+ function openAndInitialize(file2) {
25534
+ let db = openWithPragmas(file2);
25535
+ try {
25536
+ if (isForeignSqliteLineage(db)) {
25537
+ db.close();
25538
+ const backup = backupLegacyStore(file2);
25539
+ db = openWithPragmas(file2);
25540
+ akaWarn(
25541
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
25542
+ );
25543
+ }
25544
+ applyMigrations(db, file2);
25545
+ tightenPerms(file2);
25546
+ const policies = new SqlitePoliciesRepository(db);
25547
+ const installedPacks = new SqliteInstalledPacksRepository(db);
25548
+ const repositories = {
25549
+ events: new SqliteEventsRepository(db),
25550
+ findings: new SqliteFindingsRepository(db),
25551
+ policies,
25552
+ installedPacks,
25553
+ scanLedger: new SqliteScanLedgerRepository(db),
25554
+ exceptions: new SqliteExceptionsRepository(db),
25555
+ resolutions: new SqliteResolutionsRepository(db),
25556
+ ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
25557
+ security: new SqliteSecurityRepository(db),
25558
+ detections: new SqliteDetectionsRepository(db),
25559
+ shares: new SqliteSharesRepository(db),
25560
+ policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
25561
+ inventory: new SqliteInventoryRepository(db),
25562
+ inventoryAssets: new SqliteInventoryAssetsRepository(db),
25563
+ projectFiles: new SqliteProjectFilesRepository(db),
25564
+ activity: new SqliteActivityRepository(db),
25565
+ sourceProject: new SqliteSourceProjectRepository(db),
25566
+ auditEvents: new SqliteAuditEventsRepository(db),
25567
+ classifiedData: new SqliteClassifiedDataRepository(db),
25568
+ inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
25569
+ inspectionFindings: new SqliteInspectionFindingsRepository(db),
25570
+ configInventory: new SqliteConfigInventoryRepository(db)
25571
+ };
25572
+ policies.seedDefaults();
25573
+ return { db, ...repositories };
25574
+ } catch (err) {
25575
+ closeQuietly(db);
25576
+ throw err;
25577
+ }
25578
+ }
25519
25579
  function openLocalDatabase(dir) {
25520
25580
  ensureDataDirSync(dir);
25521
25581
  const file2 = join2(dir, DB_FILENAME);
25522
- let db = openWithPragmas(file2);
25523
- if (isForeignSqliteLineage(db)) {
25524
- db.close();
25525
- const backup = backupLegacyStore(file2);
25526
- db = openWithPragmas(file2);
25527
- akaWarn(
25528
- `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
25529
- );
25530
- }
25531
- applyMigrations(db, file2);
25532
- tightenPerms(file2);
25533
- const events = new SqliteEventsRepository(db);
25534
- const findings = new SqliteFindingsRepository(db);
25535
- const policies = new SqlitePoliciesRepository(db);
25536
- const installedPacks = new SqliteInstalledPacksRepository(db);
25537
- const scanLedger = new SqliteScanLedgerRepository(db);
25538
- const exceptions = new SqliteExceptionsRepository(db);
25539
- const resolutions = new SqliteResolutionsRepository(db);
25540
- const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
25541
- const security = new SqliteSecurityRepository(db);
25542
- const detections = new SqliteDetectionsRepository(db);
25543
- const shares = new SqliteSharesRepository(db);
25544
- const policyCatalog = new SqlitePolicyCatalogRepository(installedPacks);
25545
- const inventory = new SqliteInventoryRepository(db);
25546
- const inventoryAssets = new SqliteInventoryAssetsRepository(db);
25547
- const projectFiles = new SqliteProjectFilesRepository(db);
25548
- const activity = new SqliteActivityRepository(db);
25549
- const sourceProject = new SqliteSourceProjectRepository(db);
25550
- const auditEvents = new SqliteAuditEventsRepository(db);
25551
- const classifiedData = new SqliteClassifiedDataRepository(db);
25552
- const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
25553
- const inspectionFindings = new SqliteInspectionFindingsRepository(db);
25554
- const configInventory = new SqliteConfigInventoryRepository(db);
25555
- policies.seedDefaults();
25582
+ const {
25583
+ db,
25584
+ events,
25585
+ findings,
25586
+ policies,
25587
+ installedPacks,
25588
+ scanLedger,
25589
+ exceptions,
25590
+ resolutions,
25591
+ ruleProbeCache,
25592
+ security,
25593
+ detections,
25594
+ shares,
25595
+ policyCatalog,
25596
+ inventory,
25597
+ inventoryAssets,
25598
+ projectFiles,
25599
+ activity,
25600
+ sourceProject,
25601
+ auditEvents,
25602
+ classifiedData,
25603
+ inspectionDefinitions,
25604
+ inspectionFindings,
25605
+ configInventory
25606
+ } = openAndInitialize(file2);
25556
25607
  function recordCapture(event, detected) {
25557
25608
  failOpenTransaction(db, () => {
25558
25609
  const sessionId = event.metadata?.sessionId;
@@ -25764,8 +25815,9 @@ function computeFindingKey(input) {
25764
25815
 
25765
25816
  // ../packages/persistence/src/fingerprint.ts
25766
25817
  import { createHmac, randomBytes } from "crypto";
25767
- import { readFileSync as readFileSync2 } from "fs";
25818
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25768
25819
  import { join as join3 } from "path";
25820
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
25769
25821
  var KEY_FILENAME = "exception.key";
25770
25822
  var KEY_MATERIAL_BYTES = 32;
25771
25823
  function keyFilePath(dataDir2) {
@@ -25789,6 +25841,46 @@ function parseKeyFile(raw) {
25789
25841
  }
25790
25842
  return { version: version2, material: bytes };
25791
25843
  }
25844
+ var KEY_VERSION_TABLES = ["exceptions", "blocked_detections"];
25845
+ var SQLITE_ERROR = 1;
25846
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
25847
+ var FloorUnreadableError = class extends Error {
25848
+ code = "floor-unreadable";
25849
+ constructor(cause) {
25850
+ super(
25851
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
25852
+ { cause }
25853
+ );
25854
+ this.name = "FloorUnreadableError";
25855
+ }
25856
+ };
25857
+ function storedKeyVersionFloor(dataDir2) {
25858
+ const file2 = join3(dataDir2, DB_FILENAME);
25859
+ if (!existsSync3(file2)) return 0;
25860
+ let db;
25861
+ try {
25862
+ db = new DatabaseSync2(file2, { readOnly: true });
25863
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
25864
+ let floor = 0;
25865
+ for (const table of KEY_VERSION_TABLES) {
25866
+ try {
25867
+ const row = getRow(
25868
+ db.prepare(`SELECT MAX(key_version) AS v FROM ${table}`)
25869
+ );
25870
+ floor = Math.max(floor, row?.v ?? 0);
25871
+ } catch (err) {
25872
+ if (err.errcode !== SQLITE_ERROR) {
25873
+ throw new FloorUnreadableError(err);
25874
+ }
25875
+ }
25876
+ }
25877
+ return floor;
25878
+ } catch (err) {
25879
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
25880
+ } finally {
25881
+ db?.close();
25882
+ }
25883
+ }
25792
25884
  function writeKeyFile(dataDir2, key) {
25793
25885
  ensureDataDirSync(dataDir2);
25794
25886
  const file2 = keyFilePath(dataDir2);
@@ -25813,18 +25905,27 @@ function loadOrCreateFingerprintKey(dataDir2) {
25813
25905
  tightenFile(keyFilePath(dataDir2));
25814
25906
  return existing;
25815
25907
  }
25816
- return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
25908
+ return writeKeyFile(dataDir2, {
25909
+ version: storedKeyVersionFloor(dataDir2) + 1,
25910
+ material: randomBytes(KEY_MATERIAL_BYTES)
25911
+ });
25817
25912
  }
25818
25913
  function rotateFingerprintKey(dataDir2) {
25819
25914
  const existing = readFingerprintKey(dataDir2);
25820
25915
  return writeKeyFile(dataDir2, {
25821
- version: (existing?.version ?? 0) + 1,
25916
+ version: Math.max(existing?.version ?? 0, storedKeyVersionFloor(dataDir2)) + 1,
25822
25917
  material: randomBytes(KEY_MATERIAL_BYTES)
25823
25918
  });
25824
25919
  }
25825
25920
  function fingerprintValue(key, raw) {
25826
25921
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
25827
25922
  }
25923
+ function isCurrentKeyVersion(key, keyVersion) {
25924
+ return isMatchableUnder(keyVersion, keyStateOf(key));
25925
+ }
25926
+ function keyStateOf(key) {
25927
+ return key === null ? { status: "absent" } : { status: "present", version: key.version };
25928
+ }
25828
25929
 
25829
25930
  // ../packages/persistence/src/local-layout.ts
25830
25931
  import { renameSync as renameSync3 } from "fs";
@@ -25870,11 +25971,11 @@ function readJson(file2) {
25870
25971
  }
25871
25972
 
25872
25973
  // ../packages/persistence/src/warn-era-cap.ts
25873
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
25974
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
25874
25975
  import { join as join6 } from "path";
25875
25976
 
25876
25977
  // ../packages/plugin-sdk/src/config.ts
25877
- import { existsSync as existsSync4 } from "fs";
25978
+ import { existsSync as existsSync5 } from "fs";
25878
25979
  import { join as join7 } from "path";
25879
25980
 
25880
25981
  // ../packages/plugin-sdk/src/provider-env.ts
@@ -27933,7 +28034,7 @@ function bundledDetections() {
27933
28034
  }
27934
28035
 
27935
28036
  // ../packages/plugin-sdk/src/repo.ts
27936
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync } from "fs";
28037
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
27937
28038
  import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep2 } from "path";
27938
28039
  function resolveRepoIdentity(cwd) {
27939
28040
  try {
@@ -27972,7 +28073,7 @@ function resolveHeadRoot(cwd) {
27972
28073
  function findGitRoot(start) {
27973
28074
  let dir = start;
27974
28075
  for (; ; ) {
27975
- if (existsSync5(join8(dir, ".git"))) return dir;
28076
+ if (existsSync6(join8(dir, ".git"))) return dir;
27976
28077
  const parent = dirname2(dir);
27977
28078
  if (parent === dir) return void 0;
27978
28079
  dir = parent;
@@ -27990,7 +28091,7 @@ function resolveGitContext(root) {
27990
28091
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
27991
28092
  if (!target) return void 0;
27992
28093
  const gitdir = isAbsolute(target) ? target : join8(root, target);
27993
- if (existsSync5(join8(gitdir, "config"))) {
28094
+ if (existsSync6(join8(gitdir, "config"))) {
27994
28095
  return { configPath: join8(gitdir, "config"), headRoot: root };
27995
28096
  }
27996
28097
  const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
@@ -28084,7 +28185,7 @@ function resolveNonGitProject(startDir, recognizeMarker) {
28084
28185
 
28085
28186
  // ../packages/plugin-sdk/src/project-files.ts
28086
28187
  var import_ignore = __toESM(require_ignore(), 1);
28087
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28188
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28088
28189
  import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
28089
28190
  var SKIP_DIRS = /* @__PURE__ */ new Set([
28090
28191
  ".git",
@@ -28186,7 +28287,7 @@ function resolveProjectFiles(cwd) {
28186
28287
  const fullPath = join11(dir, entry.name);
28187
28288
  if (entry.isDirectory()) {
28188
28289
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
28189
- if (existsSync6(join11(fullPath, ".git"))) continue;
28290
+ if (existsSync7(join11(fullPath, ".git"))) continue;
28190
28291
  if (visit3(fullPath, dirLayers)) return true;
28191
28292
  continue;
28192
28293
  }
@@ -28489,7 +28590,7 @@ function reinvokeArgv(subcommand, extraArgs = []) {
28489
28590
 
28490
28591
  // ../packages/local-ops/src/update-cache.ts
28491
28592
  import { spawn } from "child_process";
28492
- import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
28593
+ import { existsSync as existsSync8, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
28493
28594
  import { join as join14 } from "path";
28494
28595
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
28495
28596
  function cachePath(home) {
@@ -28497,7 +28598,7 @@ function cachePath(home) {
28497
28598
  }
28498
28599
  function readCache(home) {
28499
28600
  const path = cachePath(home);
28500
- if (!existsSync7(path)) return null;
28601
+ if (!existsSync8(path)) return null;
28501
28602
  try {
28502
28603
  const raw = JSON.parse(readFileSync9(path, "utf8"));
28503
28604
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
@@ -28507,7 +28608,7 @@ function readCache(home) {
28507
28608
  }
28508
28609
  }
28509
28610
  function writeCache(home, cache) {
28510
- if (!existsSync7(dataDir(home))) return;
28611
+ if (!existsSync8(dataDir(home))) return;
28511
28612
  try {
28512
28613
  writeFileSync5(cachePath(home), `${JSON.stringify(cache, null, 2)}
28513
28614
  `);
@@ -28672,7 +28773,7 @@ function commandsHelp() {
28672
28773
  }
28673
28774
 
28674
28775
  // src/commands/check-updates.ts
28675
- import { existsSync as existsSync8 } from "fs";
28776
+ import { existsSync as existsSync9 } from "fs";
28676
28777
  import { parseArgs } from "util";
28677
28778
 
28678
28779
  // src/lib/args.ts
@@ -28703,7 +28804,7 @@ function runCheckUpdates(argv) {
28703
28804
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
28704
28805
  `);
28705
28806
  }
28706
- if (existsSync8(dataDir(home))) {
28807
+ if (existsSync9(dataDir(home))) {
28707
28808
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
28708
28809
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
28709
28810
  }
@@ -28857,7 +28958,7 @@ function runCompletion(argv) {
28857
28958
 
28858
28959
  // src/commands/dashboard.ts
28859
28960
  import { spawn as spawn3 } from "child_process";
28860
- import { existsSync as existsSync9 } from "fs";
28961
+ import { existsSync as existsSync10 } from "fs";
28861
28962
  import { createRequire as createRequire2 } from "module";
28862
28963
  import { createServer } from "net";
28863
28964
  import { dirname as dirname5, join as join15 } from "path";
@@ -28937,7 +29038,7 @@ async function runDashboard(argv) {
28937
29038
  const bundledServer = [
28938
29039
  join15(base, "web-ui", "server.js"),
28939
29040
  join15(base, "web-ui", "web-ui", "server.js")
28940
- ].find((p) => existsSync9(p));
29041
+ ].find((p) => existsSync10(p));
28941
29042
  if (bundledServer) {
28942
29043
  launchStandalone(bundledServer, port, shouldOpen, url2);
28943
29044
  return;
@@ -28956,7 +29057,7 @@ async function runDashboard(argv) {
28956
29057
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
28957
29058
  `);
28958
29059
  };
28959
- if (existsSync9(join15(webUiDir, ".next", "BUILD_ID"))) {
29060
+ if (existsSync10(join15(webUiDir, ".next", "BUILD_ID"))) {
28960
29061
  startNext();
28961
29062
  return;
28962
29063
  }
@@ -29473,10 +29574,29 @@ function printGranted(io, ex) {
29473
29574
  `
29474
29575
  );
29475
29576
  }
29476
- function blockedLine(entry) {
29477
- return `${entry.reference} ${entry.maskedValue} ${entry.ruleId} ${formatRelative(entry.blockedAt)}`;
29577
+ function blockedLine(entry, key) {
29578
+ const line = `${entry.reference} ${entry.maskedValue} ${entry.ruleId} ${formatRelative(entry.blockedAt)}`;
29579
+ return isCurrentKeyVersion(key, entry.keyVersion) ? line : `${line} [not approvable: recorded under key v${String(entry.keyVersion)}]`;
29580
+ }
29581
+ function keyAccessHint(err, dir) {
29582
+ const code = err?.code;
29583
+ if (code === "floor-unreadable") {
29584
+ return `The local store could not be read, and a key minted without it could reuse a version already in use \u2014 fix the store first (${dir}/aka.db).`;
29585
+ }
29586
+ return typeof code === "string" ? `Check the permissions on ${dir}/exception.key \u2014 do not delete it, that invalidates every existing grant.` : `Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`;
29587
+ }
29588
+ function readKeyForApprove(dir) {
29589
+ try {
29590
+ return readFingerprintKey(dir);
29591
+ } catch (err) {
29592
+ const message = err instanceof Error ? err.message : String(err);
29593
+ throw new Error(`cannot read the fingerprint key: ${message}
29594
+ ${keyAccessHint(err, dir)}`, {
29595
+ cause: err
29596
+ });
29597
+ }
29478
29598
  }
29479
- async function pickBlocked(entries, selector, io, dir) {
29599
+ async function pickBlocked(entries, selector, io, key) {
29480
29600
  if (selector !== void 0) {
29481
29601
  const matches = entries.filter(
29482
29602
  (e) => e.reference.startsWith(selector) || e.maskedValue === selector
@@ -29489,10 +29609,14 @@ async function pickBlocked(entries, selector, io, dir) {
29489
29609
  `the selector matches ${String(matches.length)} recent blocks \u2014 run 'aka exception approve' bare to pick from the list`
29490
29610
  );
29491
29611
  }
29492
- const key = loadOrCreateFingerprintKey(dir);
29612
+ if (key === null) {
29613
+ throw new Error(
29614
+ `no blocked detection matches that reference, and matching by value needs the fingerprint key \u2014 none exists on this machine`
29615
+ );
29616
+ }
29493
29617
  const fingerprint = fingerprintValue(key, selector);
29494
29618
  const byValue = entries.filter(
29495
- (e) => e.keyVersion === key.version && e.valueFingerprint === fingerprint
29619
+ (e) => isCurrentKeyVersion(key, e.keyVersion) && e.valueFingerprint === fingerprint
29496
29620
  );
29497
29621
  const newest = byValue[0];
29498
29622
  if (newest) {
@@ -29511,20 +29635,20 @@ async function pickBlocked(entries, selector, io, dir) {
29511
29635
  const only = entries[0];
29512
29636
  if (entries.length === 1 && only) {
29513
29637
  io.out(`Approving the only recent block:
29514
- ${blockedLine(only)}
29638
+ ${blockedLine(only, key)}
29515
29639
  `);
29516
29640
  return only;
29517
29641
  }
29518
29642
  if (!io.isInteractive) {
29519
29643
  io.err(`Blocked in the last 30 minutes:
29520
29644
  `);
29521
- for (const entry of entries) io.err(` ${blockedLine(entry)}
29645
+ for (const entry of entries) io.err(` ${blockedLine(entry, key)}
29522
29646
  `);
29523
29647
  throw new Error("pass the reference to approve: aka exception approve <reference>");
29524
29648
  }
29525
29649
  io.out("Blocked in the last 30 minutes:\n");
29526
29650
  entries.forEach((entry, i) => {
29527
- io.out(` ${String(i + 1)}) ${blockedLine(entry)}
29651
+ io.out(` ${String(i + 1)}) ${blockedLine(entry, key)}
29528
29652
  `);
29529
29653
  });
29530
29654
  const answer = await io.ask(`Which one? [1-${String(entries.length)}]: `);
@@ -29556,7 +29680,13 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
29556
29680
  return;
29557
29681
  }
29558
29682
  const selector = positionals[0]?.trim();
29559
- const entry = await pickBlocked(entries, selector === "" ? void 0 : selector, io, dir);
29683
+ const key = readKeyForApprove(dir);
29684
+ const entry = await pickBlocked(entries, selector === "" ? void 0 : selector, io, key);
29685
+ if (!isCurrentKeyVersion(key, entry.keyVersion)) {
29686
+ throw new Error(
29687
+ key === null ? `cannot approve ${entry.reference}: the fingerprint key file is missing, so the fingerprint recorded for it can no longer be matched \u2014 trigger the detection again` : `cannot approve ${entry.reference}: it was blocked under fingerprint key v${String(entry.keyVersion)} and the key is now v${String(key.version)}, so a grant from it could never match \u2014 trigger the detection again`
29688
+ );
29689
+ }
29560
29690
  const { scope, reason } = await resolveScopeAndReason(values, io);
29561
29691
  if (scope.scope === "permanent") {
29562
29692
  await confirmPermanent(io, entry.maskedValue, values.yes === true);
@@ -29869,11 +29999,8 @@ written under the old key simply stop matching. They remain listed for audit.
29869
29999
  version2 = rotateFingerprintKey(dir).version;
29870
30000
  } catch (err) {
29871
30001
  const message = err instanceof Error ? err.message : String(err);
29872
- throw new Error(
29873
- `cannot rotate: ${message}
29874
- Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`,
29875
- { cause: err }
29876
- );
30002
+ throw new Error(`cannot rotate: ${message}
30003
+ ${keyAccessHint(err, dir)}`, { cause: err });
29877
30004
  }
29878
30005
  io.out(
29879
30006
  `Fingerprint key rotated \u2014 new key version v${String(version2)}.
@@ -29907,13 +30034,13 @@ async function runException(argv, io = terminalPrompter()) {
29907
30034
  }
29908
30035
 
29909
30036
  // src/commands/init.ts
29910
- import { existsSync as existsSync11, statSync as statSync7 } from "fs";
30037
+ import { existsSync as existsSync12, statSync as statSync7 } from "fs";
29911
30038
  import { join as join16 } from "path";
29912
30039
  import * as readline from "readline/promises";
29913
30040
  import { parseArgs as parseArgs6 } from "util";
29914
30041
 
29915
30042
  // src/commands/plugins.ts
29916
- import { existsSync as existsSync10 } from "fs";
30043
+ import { existsSync as existsSync11 } from "fs";
29917
30044
  import { parseArgs as parseArgs5 } from "util";
29918
30045
  function runPlugins(argv) {
29919
30046
  const [sub, ...rest] = argv;
@@ -29930,7 +30057,7 @@ async function listPlugins(argv) {
29930
30057
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
29931
30058
  const home = homeBase(values.home);
29932
30059
  const active = /* @__PURE__ */ new Set();
29933
- if (existsSync10(dbPath(home))) {
30060
+ if (existsSync11(dbPath(home))) {
29934
30061
  const db = openLocalDatabase(dataDir(home));
29935
30062
  try {
29936
30063
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -30041,7 +30168,7 @@ async function runInit(argv) {
30041
30168
  const settings = settingsDir(home);
30042
30169
  ensureLayoutDirSync(settings);
30043
30170
  const settingsFile = join16(settings, "settings.json");
30044
- const settingsCreated = !existsSync11(settingsFile);
30171
+ const settingsCreated = !existsSync12(settingsFile);
30045
30172
  if (settingsCreated) {
30046
30173
  writeOwnerOnlyFileSync(
30047
30174
  settingsFile,
@@ -31069,7 +31196,50 @@ function applyPluginUpdate2(status) {
31069
31196
  return ok;
31070
31197
  }
31071
31198
 
31072
- // src/cli.ts
31199
+ // src/lib/external-dispatch.ts
31200
+ import { spawnSync as spawnSync2 } from "child_process";
31201
+ import { constants } from "os";
31202
+ function commandOnPath(command) {
31203
+ const probe = spawnSync2("/bin/sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", command], {
31204
+ stdio: "ignore"
31205
+ });
31206
+ return probe.status === 0;
31207
+ }
31208
+ function isExternalCommandName(name) {
31209
+ return /^[a-z][a-z0-9-]*$/.test(name);
31210
+ }
31211
+ function shouldDispatchExternal(command, isBuiltin) {
31212
+ return !isBuiltin && isExternalCommandName(command);
31213
+ }
31214
+ function externalDispatchSupported(platform2 = process.platform) {
31215
+ return platform2 !== "win32";
31216
+ }
31217
+ function signalExitCode(signal) {
31218
+ const number4 = signal ? constants.signals[signal] : void 0;
31219
+ return number4 === void 0 ? 1 : 128 + number4;
31220
+ }
31221
+ function dispatchExternal(command, argv, deps = {}) {
31222
+ const spawn4 = deps.spawn ?? spawnSync2;
31223
+ const platform2 = deps.platform ?? process.platform;
31224
+ const exists = deps.exists ?? commandOnPath;
31225
+ if (!externalDispatchSupported(platform2)) return { found: false, status: 1 };
31226
+ const target = `aka-${command}`;
31227
+ const result = spawn4(target, argv, { stdio: "inherit" });
31228
+ if (result.error) {
31229
+ if (result.error.code === "ENOENT" && !exists(target)) return { found: false, status: 1 };
31230
+ process.stderr.write(`aka: ${target}: ${result.error.message}
31231
+ `);
31232
+ return { found: true, status: 1 };
31233
+ }
31234
+ if (result.status === null) {
31235
+ if (result.signal) process.stderr.write(`aka: ${target}: terminated by ${result.signal}
31236
+ `);
31237
+ return { found: true, status: signalExitCode(result.signal) };
31238
+ }
31239
+ return { found: true, status: result.status };
31240
+ }
31241
+
31242
+ // src/main.ts
31073
31243
  var COMMANDS = {
31074
31244
  init: runInit,
31075
31245
  scan: runScan,
@@ -31106,7 +31276,9 @@ Usage: aka <command> [options]
31106
31276
 
31107
31277
  Commands:
31108
31278
  ${commandsHelp()}
31109
-
31279
+ ${externalDispatchSupported() ? `
31280
+ Anything else runs an external 'aka-<command>' from your PATH (so 'aka claude' runs aka-claude).
31281
+ ` : ""}
31110
31282
  Options:
31111
31283
  --home <dir> Use an alternate AKA home (default: ~/.aka)
31112
31284
  --no-update-check Skip the post-command "updates available" notice
@@ -31122,8 +31294,7 @@ function homeFromArgv(argv) {
31122
31294
  }
31123
31295
  return homeBase(void 0);
31124
31296
  }
31125
- async function main() {
31126
- const argv = process.argv.slice(2);
31297
+ async function main(argv = process.argv.slice(2), dispatchDeps = {}) {
31127
31298
  if (argv[0] === "-v" || argv[0] === "--version" || argv[0] === "version") {
31128
31299
  process.stdout.write(`${cliVersion() ?? "unknown"}
31129
31300
  `);
@@ -31136,7 +31307,14 @@ async function main() {
31136
31307
  }
31137
31308
  const noUpdateCheck = rawRest.includes("--no-update-check");
31138
31309
  const rest = rawRest.filter((a) => a !== "--no-update-check");
31139
- const handler = COMMANDS[command];
31310
+ const handler = Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : void 0;
31311
+ if (shouldDispatchExternal(command, handler !== void 0)) {
31312
+ const external = dispatchExternal(command, rawRest, dispatchDeps);
31313
+ if (external.found) {
31314
+ process.exitCode = external.status;
31315
+ return;
31316
+ }
31317
+ }
31140
31318
  if (!handler) {
31141
31319
  process.stderr.write(`aka: unknown command '${command}'
31142
31320
 
@@ -31152,6 +31330,8 @@ ${USAGE}`);
31152
31330
  }
31153
31331
  }
31154
31332
  }
31333
+
31334
+ // src/cli.ts
31155
31335
  main().catch((err) => {
31156
31336
  process.stderr.write(`aka: ${err instanceof Error ? err.message : String(err)}
31157
31337
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akasecurity/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "AI Traffic Control — local-first CLI to inspect and govern AI agent traffic on your machine",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,12 +41,12 @@
41
41
  "typescript": "^5.8.0",
42
42
  "vitest": "^4.1.10",
43
43
  "@akasecurity/dashboard-ui": "0.8.0",
44
- "@akasecurity/eslint-config": "0.8.0",
44
+ "@akasecurity/detections": "0.8.0",
45
45
  "@akasecurity/local-ops": "0.8.0",
46
+ "@akasecurity/eslint-config": "0.8.0",
46
47
  "@akasecurity/plugin-sdk": "0.8.0",
47
- "@akasecurity/schema": "0.8.0",
48
- "@akasecurity/detections": "0.8.0",
49
48
  "@akasecurity/persistence": "0.8.0",
49
+ "@akasecurity/schema": "0.8.0",
50
50
  "@akasecurity/web-ui": "0.8.0"
51
51
  },
52
52
  "scripts": {
@@ -1 +1 @@
1
- mAOCejrDqkMoDfzjWr6qu
1
+ X_Z1LZQpnGXTuZXKkv4Vb