bashbros 0.1.2 → 0.1.4

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 (65) hide show
  1. package/README.md +727 -265
  2. package/dist/adapters-JAZGGNVP.js +9 -0
  3. package/dist/chunk-4XZ64P4V.js +47 -0
  4. package/dist/chunk-4XZ64P4V.js.map +1 -0
  5. package/dist/{chunk-XCZMQRSX.js → chunk-7OEWYFN3.js} +745 -541
  6. package/dist/chunk-7OEWYFN3.js.map +1 -0
  7. package/dist/{chunk-SQCP6IYB.js → chunk-CG6VEHJM.js} +3 -2
  8. package/dist/chunk-CG6VEHJM.js.map +1 -0
  9. package/dist/{chunk-DLP2O6PN.js → chunk-EMLEJVJZ.js} +102 -1
  10. package/dist/chunk-EMLEJVJZ.js.map +1 -0
  11. package/dist/chunk-IUUBCPMV.js +166 -0
  12. package/dist/chunk-IUUBCPMV.js.map +1 -0
  13. package/dist/chunk-J6ONXY6N.js +146 -0
  14. package/dist/chunk-J6ONXY6N.js.map +1 -0
  15. package/dist/chunk-KYDMPE4N.js +224 -0
  16. package/dist/chunk-KYDMPE4N.js.map +1 -0
  17. package/dist/chunk-LJE4EPIU.js +56 -0
  18. package/dist/chunk-LJE4EPIU.js.map +1 -0
  19. package/dist/chunk-LZYW7XQO.js +339 -0
  20. package/dist/chunk-LZYW7XQO.js.map +1 -0
  21. package/dist/{chunk-YUMNBQAY.js → chunk-RDNSS3ME.js} +587 -12
  22. package/dist/chunk-RDNSS3ME.js.map +1 -0
  23. package/dist/{chunk-BW6XCOJH.js → chunk-RTZ4QWG2.js} +2 -2
  24. package/dist/chunk-RTZ4QWG2.js.map +1 -0
  25. package/dist/chunk-SDN6TAGD.js +157 -0
  26. package/dist/chunk-SDN6TAGD.js.map +1 -0
  27. package/dist/chunk-T5ONCUHZ.js +198 -0
  28. package/dist/chunk-T5ONCUHZ.js.map +1 -0
  29. package/dist/cli.js +1182 -251
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{config-JLLOTFLI.js → config-I5NCK3RJ.js} +2 -2
  32. package/dist/copilot-cli-5WJWK5YT.js +9 -0
  33. package/dist/{db-OBKEXRTP.js → db-ETWTBXAE.js} +2 -2
  34. package/dist/db-checks-2YOVECD4.js +133 -0
  35. package/dist/db-checks-2YOVECD4.js.map +1 -0
  36. package/dist/{display-6LZ2HBCU.js → display-UH7KEHOW.js} +3 -3
  37. package/dist/display-UH7KEHOW.js.map +1 -0
  38. package/dist/gemini-cli-3563EELZ.js +9 -0
  39. package/dist/gemini-cli-3563EELZ.js.map +1 -0
  40. package/dist/index.d.ts +195 -72
  41. package/dist/index.js +119 -398
  42. package/dist/index.js.map +1 -1
  43. package/dist/{ollama-HY35OHW4.js → ollama-5JVKNFOV.js} +2 -2
  44. package/dist/ollama-5JVKNFOV.js.map +1 -0
  45. package/dist/opencode-DRCY275R.js +9 -0
  46. package/dist/opencode-DRCY275R.js.map +1 -0
  47. package/dist/profiles-7CLN6TAT.js +9 -0
  48. package/dist/profiles-7CLN6TAT.js.map +1 -0
  49. package/dist/setup-YS27MOPE.js +124 -0
  50. package/dist/setup-YS27MOPE.js.map +1 -0
  51. package/dist/static/index.html +4815 -2007
  52. package/dist/store-WJ5Y7MOE.js +9 -0
  53. package/dist/store-WJ5Y7MOE.js.map +1 -0
  54. package/dist/writer-3NAVABN6.js +12 -0
  55. package/dist/writer-3NAVABN6.js.map +1 -0
  56. package/package.json +77 -68
  57. package/dist/chunk-BW6XCOJH.js.map +0 -1
  58. package/dist/chunk-DLP2O6PN.js.map +0 -1
  59. package/dist/chunk-SQCP6IYB.js.map +0 -1
  60. package/dist/chunk-XCZMQRSX.js.map +0 -1
  61. package/dist/chunk-YUMNBQAY.js.map +0 -1
  62. /package/dist/{config-JLLOTFLI.js.map → adapters-JAZGGNVP.js.map} +0 -0
  63. /package/dist/{db-OBKEXRTP.js.map → config-I5NCK3RJ.js.map} +0 -0
  64. /package/dist/{display-6LZ2HBCU.js.map → copilot-cli-5WJWK5YT.js.map} +0 -0
  65. /package/dist/{ollama-HY35OHW4.js.map → db-ETWTBXAE.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  // src/dashboard/db.ts
4
4
  import Database from "better-sqlite3";
5
5
  import { randomUUID } from "crypto";
6
- var DashboardDB = class {
6
+ var DashboardDB = class _DashboardDB {
7
7
  db;
8
8
  constructor(dbPath = ".bashbros.db") {
9
9
  this.db = new Database(dbPath);
@@ -85,7 +85,7 @@ var DashboardDB = class {
85
85
  this.db.exec(`
86
86
  CREATE TABLE IF NOT EXISTS commands (
87
87
  id TEXT PRIMARY KEY,
88
- session_id TEXT NOT NULL,
88
+ session_id TEXT,
89
89
  timestamp TEXT NOT NULL,
90
90
  command TEXT NOT NULL,
91
91
  allowed INTEGER NOT NULL,
@@ -97,6 +97,7 @@ var DashboardDB = class {
97
97
  FOREIGN KEY (session_id) REFERENCES sessions(id)
98
98
  )
99
99
  `);
100
+ this.migrateCommandsNullableSessionId();
100
101
  this.db.exec(`
101
102
  CREATE TABLE IF NOT EXISTS bro_events (
102
103
  id TEXT PRIMARY KEY,
@@ -122,6 +123,34 @@ var DashboardDB = class {
122
123
  project_type TEXT
123
124
  )
124
125
  `);
126
+ this.db.exec(`
127
+ CREATE TABLE IF NOT EXISTS tool_uses (
128
+ id TEXT PRIMARY KEY,
129
+ timestamp TEXT NOT NULL,
130
+ tool_name TEXT NOT NULL,
131
+ tool_input TEXT NOT NULL,
132
+ tool_output TEXT NOT NULL,
133
+ exit_code INTEGER,
134
+ success INTEGER,
135
+ cwd TEXT NOT NULL,
136
+ repo_name TEXT,
137
+ repo_path TEXT
138
+ )
139
+ `);
140
+ this.db.exec(`
141
+ CREATE TABLE IF NOT EXISTS adapter_events (
142
+ id TEXT PRIMARY KEY,
143
+ timestamp TEXT NOT NULL,
144
+ adapter_name TEXT NOT NULL,
145
+ base_model TEXT NOT NULL,
146
+ purpose TEXT NOT NULL,
147
+ action TEXT NOT NULL,
148
+ success INTEGER NOT NULL
149
+ )
150
+ `);
151
+ this.migrateToolUsesAddSessionId();
152
+ this.migrateSessionsAddMode();
153
+ this.migrateSessionsAddRepoName();
125
154
  this.db.exec(`
126
155
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
127
156
  CREATE INDEX IF NOT EXISTS idx_sessions_start_time ON sessions(start_time);
@@ -131,6 +160,10 @@ var DashboardDB = class {
131
160
  CREATE INDEX IF NOT EXISTS idx_bro_events_session_id ON bro_events(session_id);
132
161
  CREATE INDEX IF NOT EXISTS idx_bro_events_timestamp ON bro_events(timestamp);
133
162
  CREATE INDEX IF NOT EXISTS idx_bro_status_timestamp ON bro_status(timestamp);
163
+ CREATE INDEX IF NOT EXISTS idx_tool_uses_timestamp ON tool_uses(timestamp);
164
+ CREATE INDEX IF NOT EXISTS idx_tool_uses_tool_name ON tool_uses(tool_name);
165
+ CREATE INDEX IF NOT EXISTS idx_tool_uses_session_id ON tool_uses(session_id);
166
+ CREATE INDEX IF NOT EXISTS idx_adapter_events_timestamp ON adapter_events(timestamp);
134
167
  `);
135
168
  }
136
169
  // ─────────────────────────────────────────────────────────────
@@ -361,10 +394,10 @@ var DashboardDB = class {
361
394
  const id = randomUUID();
362
395
  const startTime = (/* @__PURE__ */ new Date()).toISOString();
363
396
  const stmt = this.db.prepare(`
364
- INSERT INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir)
365
- VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?)
397
+ INSERT INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir, repo_name)
398
+ VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?, ?)
366
399
  `);
367
- stmt.run(id, input.agent, input.pid, startTime, input.workingDir);
400
+ stmt.run(id, input.agent, input.pid, startTime, input.workingDir, input.repoName ?? null);
368
401
  return id;
369
402
  }
370
403
  updateSession(id, updates) {
@@ -443,6 +476,44 @@ var DashboardDB = class {
443
476
  if (!row) return null;
444
477
  return this.rowToSession(row);
445
478
  }
479
+ /**
480
+ * Insert a session with an externally-provided ID (for hook-mode sessions).
481
+ * Uses INSERT OR IGNORE for race-safe concurrent hook calls.
482
+ */
483
+ insertSessionWithId(id, input) {
484
+ const startTime = (/* @__PURE__ */ new Date()).toISOString();
485
+ const stmt = this.db.prepare(`
486
+ INSERT OR IGNORE INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir, mode, repo_name)
487
+ VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?, ?, ?)
488
+ `);
489
+ stmt.run(id, input.agent, input.pid, startTime, input.workingDir, input.mode ?? "hook", input.repoName ?? null);
490
+ return id;
491
+ }
492
+ /**
493
+ * Get ALL active sessions, ordered by start_time DESC.
494
+ * Used by the multi-session dashboard.
495
+ */
496
+ getActiveSessions() {
497
+ const stmt = this.db.prepare(`
498
+ SELECT * FROM sessions WHERE status = 'active' ORDER BY start_time DESC
499
+ `);
500
+ const rows = stmt.all();
501
+ return rows.map((row) => this.rowToSession(row));
502
+ }
503
+ /**
504
+ * Atomically increment session counters in a single UPDATE.
505
+ * Race-safe for concurrent hook processes (SQLite serializes writes).
506
+ */
507
+ incrementSessionCommand(id, blocked, riskScore) {
508
+ const stmt = this.db.prepare(`
509
+ UPDATE sessions SET
510
+ command_count = command_count + 1,
511
+ blocked_count = blocked_count + ?,
512
+ avg_risk_score = (avg_risk_score * command_count + ?) / (command_count + 1)
513
+ WHERE id = ?
514
+ `);
515
+ stmt.run(blocked ? 1 : 0, riskScore, id);
516
+ }
446
517
  rowToSession(row) {
447
518
  return {
448
519
  id: row.id,
@@ -454,7 +525,8 @@ var DashboardDB = class {
454
525
  commandCount: row.command_count,
455
526
  blockedCount: row.blocked_count,
456
527
  avgRiskScore: row.avg_risk_score,
457
- workingDir: row.working_dir
528
+ workingDir: row.working_dir,
529
+ repoName: row.repo_name ?? null
458
530
  };
459
531
  }
460
532
  // ─────────────────────────────────────────────────────────────
@@ -469,7 +541,7 @@ var DashboardDB = class {
469
541
  `);
470
542
  stmt.run(
471
543
  id,
472
- input.sessionId,
544
+ input.sessionId ?? null,
473
545
  timestamp,
474
546
  input.command,
475
547
  input.allowed ? 1 : 0,
@@ -522,12 +594,17 @@ var DashboardDB = class {
522
594
  }
523
595
  getLiveCommands(limit = 20) {
524
596
  const stmt = this.db.prepare(`
525
- SELECT * FROM commands
526
- ORDER BY timestamp DESC
597
+ SELECT c.*, s.repo_name, s.working_dir
598
+ FROM commands c
599
+ LEFT JOIN sessions s ON c.session_id = s.id
600
+ ORDER BY c.timestamp DESC
527
601
  LIMIT ?
528
602
  `);
529
603
  const rows = stmt.all(limit);
530
- return rows.map((row) => this.rowToCommand(row));
604
+ return rows.map((row) => ({
605
+ ...this.rowToCommand(row),
606
+ repoName: row.repo_name ?? (row.working_dir ? row.working_dir.split(/[/\\]/).pop() ?? null : null)
607
+ }));
531
608
  }
532
609
  rowToCommand(row) {
533
610
  return {
@@ -637,6 +714,132 @@ var DashboardDB = class {
637
714
  };
638
715
  }
639
716
  // ─────────────────────────────────────────────────────────────
717
+ // Adapter Events
718
+ // ─────────────────────────────────────────────────────────────
719
+ insertAdapterEvent(input) {
720
+ const id = randomUUID();
721
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
722
+ const stmt = this.db.prepare(`
723
+ INSERT INTO adapter_events (id, timestamp, adapter_name, base_model, purpose, action, success)
724
+ VALUES (?, ?, ?, ?, ?, ?, ?)
725
+ `);
726
+ stmt.run(
727
+ id,
728
+ timestamp,
729
+ input.adapterName,
730
+ input.baseModel,
731
+ input.purpose,
732
+ input.action,
733
+ input.success ? 1 : 0
734
+ );
735
+ return id;
736
+ }
737
+ getAdapterEvents(limit = 50) {
738
+ const stmt = this.db.prepare(`
739
+ SELECT * FROM adapter_events
740
+ ORDER BY timestamp DESC
741
+ LIMIT ?
742
+ `);
743
+ const rows = stmt.all(limit);
744
+ return rows.map((row) => ({
745
+ id: row.id,
746
+ timestamp: new Date(row.timestamp),
747
+ adapterName: row.adapter_name,
748
+ baseModel: row.base_model,
749
+ purpose: row.purpose,
750
+ action: row.action,
751
+ success: row.success === 1
752
+ }));
753
+ }
754
+ // ─────────────────────────────────────────────────────────────
755
+ // Tool Uses
756
+ // ─────────────────────────────────────────────────────────────
757
+ insertToolUse(input) {
758
+ const id = randomUUID();
759
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
760
+ const stmt = this.db.prepare(`
761
+ INSERT INTO tool_uses (id, timestamp, tool_name, tool_input, tool_output, exit_code, success, cwd, repo_name, repo_path, session_id)
762
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
763
+ `);
764
+ stmt.run(
765
+ id,
766
+ timestamp,
767
+ input.toolName,
768
+ input.toolInput.substring(0, 5e4),
769
+ // Truncate very long inputs
770
+ input.toolOutput.substring(0, 5e4),
771
+ // Truncate very long outputs
772
+ input.exitCode ?? null,
773
+ input.success === void 0 ? null : input.success ? 1 : 0,
774
+ input.cwd,
775
+ input.repoName ?? null,
776
+ input.repoPath ?? null,
777
+ input.sessionId ?? null
778
+ );
779
+ return id;
780
+ }
781
+ getToolUses(filter = {}) {
782
+ const conditions = [];
783
+ const params = [];
784
+ if (filter.toolName) {
785
+ conditions.push("tool_name = ?");
786
+ params.push(filter.toolName);
787
+ }
788
+ if (filter.sessionId) {
789
+ conditions.push("session_id = ?");
790
+ params.push(filter.sessionId);
791
+ }
792
+ if (filter.since) {
793
+ conditions.push("timestamp >= ?");
794
+ params.push(filter.since.toISOString());
795
+ }
796
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
797
+ const limit = filter.limit ?? 100;
798
+ const offset = filter.offset ?? 0;
799
+ const stmt = this.db.prepare(`
800
+ SELECT * FROM tool_uses
801
+ ${whereClause}
802
+ ORDER BY timestamp DESC
803
+ LIMIT ? OFFSET ?
804
+ `);
805
+ params.push(limit, offset);
806
+ const rows = stmt.all(...params);
807
+ return rows.map((row) => ({
808
+ id: row.id,
809
+ timestamp: new Date(row.timestamp),
810
+ toolName: row.tool_name,
811
+ toolInput: row.tool_input,
812
+ toolOutput: row.tool_output,
813
+ exitCode: row.exit_code,
814
+ success: row.success === null ? null : row.success === 1,
815
+ cwd: row.cwd,
816
+ repoName: row.repo_name,
817
+ repoPath: row.repo_path
818
+ }));
819
+ }
820
+ getLiveToolUses(limit = 50) {
821
+ return this.getToolUses({ limit });
822
+ }
823
+ getToolUseStats() {
824
+ const totalRow = this.db.prepare("SELECT COUNT(*) as count FROM tool_uses").get();
825
+ const toolRows = this.db.prepare(`
826
+ SELECT tool_name, COUNT(*) as count FROM tool_uses GROUP BY tool_name ORDER BY count DESC
827
+ `).all();
828
+ const byTool = {};
829
+ for (const row of toolRows) {
830
+ byTool[row.tool_name] = row.count;
831
+ }
832
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
833
+ const last24hRow = this.db.prepare(`
834
+ SELECT COUNT(*) as count FROM tool_uses WHERE timestamp >= ?
835
+ `).get(oneDayAgo);
836
+ return {
837
+ totalUses: totalRow.count,
838
+ byTool,
839
+ last24h: last24hRow.count
840
+ };
841
+ }
842
+ // ─────────────────────────────────────────────────────────────
640
843
  // Session Metrics
641
844
  // ─────────────────────────────────────────────────────────────
642
845
  getSessionMetrics(sessionId) {
@@ -673,6 +876,61 @@ var DashboardDB = class {
673
876
  };
674
877
  }
675
878
  // ─────────────────────────────────────────────────────────────
879
+ // Security Summary
880
+ // ─────────────────────────────────────────────────────────────
881
+ getSecuritySummary() {
882
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
883
+ const totalRow = this.db.prepare(`
884
+ SELECT COUNT(*) as count FROM commands WHERE timestamp >= ?
885
+ `).get(oneDayAgo);
886
+ const blockedRow = this.db.prepare(`
887
+ SELECT COUNT(*) as count FROM commands WHERE timestamp >= ? AND allowed = 0
888
+ `).get(oneDayAgo);
889
+ const avgRow = this.db.prepare(`
890
+ SELECT AVG(risk_score) as avg FROM commands WHERE timestamp >= ?
891
+ `).get(oneDayAgo);
892
+ const riskRows = this.db.prepare(`
893
+ SELECT risk_level, COUNT(*) as count FROM commands WHERE timestamp >= ? GROUP BY risk_level
894
+ `).all(oneDayAgo);
895
+ const riskDistribution = {};
896
+ for (const row of riskRows) {
897
+ riskDistribution[row.risk_level] = row.count;
898
+ }
899
+ const highRiskRow = this.db.prepare(`
900
+ SELECT COUNT(*) as count FROM commands WHERE timestamp >= ? AND risk_level IN ('dangerous', 'critical')
901
+ `).get(oneDayAgo);
902
+ const violationRows = this.db.prepare(`
903
+ SELECT violations FROM commands WHERE timestamp >= ? AND allowed = 0 AND violations != '[]'
904
+ `).all(oneDayAgo);
905
+ const typeCounts = {};
906
+ for (const row of violationRows) {
907
+ try {
908
+ const violations = JSON.parse(row.violations);
909
+ for (const v of violations) {
910
+ const type = v.includes(":") ? v.split(":")[0] : v;
911
+ typeCounts[type] = (typeCounts[type] || 0) + 1;
912
+ }
913
+ } catch {
914
+ }
915
+ }
916
+ const violationTypes = Object.entries(typeCounts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count);
917
+ return {
918
+ totalCommands24h: totalRow.count,
919
+ blockedCount24h: blockedRow.count,
920
+ avgRiskScore24h: avgRow.avg ?? 0,
921
+ riskDistribution,
922
+ violationTypes,
923
+ highRiskCount24h: highRiskRow.count
924
+ };
925
+ }
926
+ getBlockedCommandsRecent(limit = 25) {
927
+ const stmt = this.db.prepare(`
928
+ SELECT * FROM commands WHERE allowed = 0 ORDER BY timestamp DESC LIMIT ?
929
+ `);
930
+ const rows = stmt.all(limit);
931
+ return rows.map((row) => this.rowToCommand(row));
932
+ }
933
+ // ─────────────────────────────────────────────────────────────
676
934
  // Stats
677
935
  // ─────────────────────────────────────────────────────────────
678
936
  getStats() {
@@ -735,6 +993,27 @@ var DashboardDB = class {
735
993
  };
736
994
  }
737
995
  // ─────────────────────────────────────────────────────────────
996
+ // Cross-process Query Helpers (for db-checks)
997
+ // ─────────────────────────────────────────────────────────────
998
+ getRecentCommandTexts(windowSize) {
999
+ const stmt = this.db.prepare(`
1000
+ SELECT command, timestamp FROM commands
1001
+ ORDER BY timestamp DESC
1002
+ LIMIT ?
1003
+ `);
1004
+ return stmt.all(windowSize);
1005
+ }
1006
+ getCommandCountSince(sinceISO) {
1007
+ const row = this.db.prepare(`
1008
+ SELECT COUNT(*) as count FROM commands WHERE timestamp >= ?
1009
+ `).get(sinceISO);
1010
+ return row.count;
1011
+ }
1012
+ getTotalCommandCount() {
1013
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM commands").get();
1014
+ return row.count;
1015
+ }
1016
+ // ─────────────────────────────────────────────────────────────
738
1017
  // Maintenance
739
1018
  // ─────────────────────────────────────────────────────────────
740
1019
  cleanup(olderThanDays = 30) {
@@ -751,16 +1030,312 @@ var DashboardDB = class {
751
1030
  `).run(cutoff).changes;
752
1031
  const broEventsDeleted = this.db.prepare("DELETE FROM bro_events WHERE timestamp < ?").run(cutoff).changes;
753
1032
  const broStatusDeleted = this.db.prepare("DELETE FROM bro_status WHERE timestamp < ?").run(cutoff).changes;
754
- return eventsDeleted + connectorDeleted + blocksDeleted + exposuresDeleted + commandsDeleted + sessionsDeleted + broEventsDeleted + broStatusDeleted;
1033
+ const toolUsesDeleted = this.db.prepare("DELETE FROM tool_uses WHERE timestamp < ?").run(cutoff).changes;
1034
+ return eventsDeleted + connectorDeleted + blocksDeleted + exposuresDeleted + commandsDeleted + sessionsDeleted + broEventsDeleted + broStatusDeleted + toolUsesDeleted;
1035
+ }
1036
+ /**
1037
+ * Migration: add session_id column to tool_uses table for session tracking
1038
+ */
1039
+ migrateToolUsesAddSessionId() {
1040
+ try {
1041
+ const tableInfo = this.db.pragma("table_info(tool_uses)");
1042
+ const hasSessionId = tableInfo.some((col) => col.name === "session_id");
1043
+ if (!hasSessionId) {
1044
+ this.db.exec("ALTER TABLE tool_uses ADD COLUMN session_id TEXT");
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Migration: add mode column to sessions table to distinguish watch vs hook sessions
1051
+ */
1052
+ migrateSessionsAddMode() {
1053
+ try {
1054
+ const tableInfo = this.db.pragma("table_info(sessions)");
1055
+ const hasMode = tableInfo.some((col) => col.name === "mode");
1056
+ if (!hasMode) {
1057
+ this.db.exec("ALTER TABLE sessions ADD COLUMN mode TEXT DEFAULT 'watch'");
1058
+ }
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Migration: add repo_name column to sessions table
1064
+ */
1065
+ migrateSessionsAddRepoName() {
1066
+ try {
1067
+ const tableInfo = this.db.pragma("table_info(sessions)");
1068
+ const hasRepoName = tableInfo.some((col) => col.name === "repo_name");
1069
+ if (!hasRepoName) {
1070
+ this.db.exec("ALTER TABLE sessions ADD COLUMN repo_name TEXT");
1071
+ }
1072
+ } catch {
1073
+ }
1074
+ }
1075
+ /**
1076
+ * Migration: allow NULL session_id in commands table for hook-mode recording
1077
+ * (each hook invocation is a separate process with no long-running session)
1078
+ */
1079
+ migrateCommandsNullableSessionId() {
1080
+ try {
1081
+ const tableInfo = this.db.pragma("table_info(commands)");
1082
+ const sessionCol = tableInfo.find((col) => col.name === "session_id");
1083
+ if (sessionCol && sessionCol.notnull === 1) {
1084
+ this.db.exec(`
1085
+ CREATE TABLE commands_mig (
1086
+ id TEXT PRIMARY KEY,
1087
+ session_id TEXT,
1088
+ timestamp TEXT NOT NULL,
1089
+ command TEXT NOT NULL,
1090
+ allowed INTEGER NOT NULL,
1091
+ risk_score INTEGER NOT NULL,
1092
+ risk_level TEXT NOT NULL,
1093
+ risk_factors TEXT NOT NULL,
1094
+ duration_ms INTEGER NOT NULL,
1095
+ violations TEXT NOT NULL,
1096
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
1097
+ );
1098
+ INSERT INTO commands_mig SELECT * FROM commands;
1099
+ DROP TABLE commands;
1100
+ ALTER TABLE commands_mig RENAME TO commands;
1101
+ CREATE INDEX IF NOT EXISTS idx_commands_session_id ON commands(session_id);
1102
+ CREATE INDEX IF NOT EXISTS idx_commands_timestamp ON commands(timestamp);
1103
+ CREATE INDEX IF NOT EXISTS idx_commands_allowed ON commands(allowed);
1104
+ `);
1105
+ }
1106
+ } catch {
1107
+ }
1108
+ }
1109
+ // ─────────────────────────────────────────────────────────────
1110
+ // Achievement System
1111
+ // ─────────────────────────────────────────────────────────────
1112
+ static BADGE_DEFINITIONS = [
1113
+ // Volume
1114
+ { id: "first_blood", name: "First Blood", description: "Execute commands", category: "volume", icon: "\u2694", stat: "totalCommands", tiers: [1, 100, 1e3, 1e4, 1e5] },
1115
+ { id: "marathon_runner", name: "Marathon Runner", description: "Complete sessions", category: "volume", icon: "\u{1F3C3}", stat: "totalSessions", tiers: [1, 10, 50, 200, 1e3] },
1116
+ { id: "watchdog", name: "Watchdog", description: "Time under watch", category: "volume", icon: "\u23F1", stat: "totalWatchTimeMinutes", tiers: [60, 1440, 10080, 43200, 525600] },
1117
+ // Security
1118
+ { id: "shield_bearer", name: "Shield Bearer", description: "Threats blocked", category: "security", icon: "\u{1F6E1}", stat: "totalBlocked", tiers: [1, 25, 100, 500, 2e3] },
1119
+ { id: "clean_hands", name: "Clean Hands", description: "Consecutive clean commands", category: "security", icon: "\u2728", stat: "cleanestStreak", tiers: [10, 50, 200, 1e3, 5e3] },
1120
+ { id: "risk_taker", name: "Risk Taker", description: "High risk commands executed", category: "security", icon: "\u{1F3B2}", stat: "highRiskCount", tiers: [1, 5, 10, 25, 50] },
1121
+ // Agents
1122
+ { id: "buddy_system", name: "Buddy System", description: "Use 2+ agents", category: "agents", icon: "\u{1F91D}", stat: "uniqueAgents", tiers: [2, 2, 2, 2, 2] },
1123
+ { id: "squad_up", name: "Squad Up", description: "Use multiple agents", category: "agents", icon: "\u{1F46B}", stat: "uniqueAgents", tiers: [2, 3, 4, 5, 5] },
1124
+ { id: "loyal", name: "Loyal", description: "1000 commands from one agent", category: "agents", icon: "\u{1F451}", stat: "maxAgentCommands", tiers: [100, 250, 500, 1e3, 5e3] },
1125
+ { id: "polyglot", name: "Polyglot", description: "100+ commands from multiple agents", category: "agents", icon: "\u{1F30D}", stat: "agentsWith100", tiers: [1, 2, 3, 4, 5] },
1126
+ // Behavioral
1127
+ { id: "night_owl", name: "Night Owl", description: "Commands after midnight", category: "behavioral", icon: "\u{1F989}", stat: "lateNightCount", tiers: [10, 100, 500, 2e3, 1e4] },
1128
+ { id: "speed_demon", name: "Speed Demon", description: "Commands in a single hour", category: "behavioral", icon: "\u26A1", stat: "peakHourCount", tiers: [60, 100, 150, 200, 300] },
1129
+ { id: "one_liner", name: "One-Liner", description: "Longest command (chars)", category: "behavioral", icon: "\u{1F4DD}", stat: "longestCommandLength", tiers: [500, 1e3, 2e3, 5e3, 1e4] },
1130
+ { id: "creature_of_habit", name: "Creature of Habit", description: "Same command repeated", category: "behavioral", icon: "\u{1F501}", stat: "mostUsedCommandCount", tiers: [50, 200, 500, 1e3, 5e3] },
1131
+ { id: "explorer", name: "Explorer", description: "Unique commands used", category: "behavioral", icon: "\u{1F9ED}", stat: "uniqueCommands", tiers: [25, 50, 100, 250, 500] },
1132
+ // Repo
1133
+ { id: "home_base", name: "Home Base", description: "Protect a repo", category: "repo", icon: "\u{1F3E0}", stat: "uniqueRepos", tiers: [1, 1, 1, 1, 1] },
1134
+ { id: "empire", name: "Empire", description: "Protect multiple repos", category: "repo", icon: "\u{1F3F0}", stat: "uniqueRepos", tiers: [3, 5, 10, 25, 50] }
1135
+ ];
1136
+ getAchievementStats() {
1137
+ const totalCommands = this.db.prepare("SELECT COUNT(*) as c FROM commands").get().c;
1138
+ const totalBlocked = this.db.prepare("SELECT COUNT(*) as c FROM commands WHERE allowed = 0").get().c;
1139
+ const totalSessions = this.db.prepare("SELECT COUNT(*) as c FROM sessions").get().c;
1140
+ const totalCharacters = this.db.prepare("SELECT COALESCE(SUM(LENGTH(command)), 0) as c FROM commands").get().c;
1141
+ const watchTimeRow = this.db.prepare(`
1142
+ SELECT COALESCE(SUM(
1143
+ (julianday(COALESCE(end_time, datetime('now'))) - julianday(start_time)) * 1440
1144
+ ), 0) as minutes FROM sessions
1145
+ `).get();
1146
+ const totalWatchTimeMinutes = Math.round(watchTimeRow.minutes);
1147
+ const uniqueRepos = this.db.prepare(`
1148
+ SELECT COUNT(DISTINCT repo_name) as c FROM sessions WHERE repo_name IS NOT NULL AND repo_name != ''
1149
+ `).get().c;
1150
+ const firstCommandRow = this.db.prepare("SELECT MIN(timestamp) as t FROM commands").get();
1151
+ const memberSince = firstCommandRow.t || null;
1152
+ const agentRows = this.db.prepare(`
1153
+ SELECT s.agent, COUNT(*) as count
1154
+ FROM commands c
1155
+ JOIN sessions s ON c.session_id = s.id
1156
+ GROUP BY s.agent
1157
+ ORDER BY count DESC
1158
+ `).all();
1159
+ const agentBreakdown = {};
1160
+ for (const row of agentRows) {
1161
+ agentBreakdown[row.agent] = row.count;
1162
+ }
1163
+ const uniqueAgents = Object.keys(agentBreakdown).length;
1164
+ const maxAgentCommands = agentRows.length > 0 ? agentRows[0].count : 0;
1165
+ const favoriteAgent = agentRows.length > 0 ? agentRows[0].agent : null;
1166
+ const agentsWith100 = agentRows.filter((r) => r.count >= 100).length;
1167
+ const mostUsedRow = this.db.prepare(`
1168
+ SELECT command, COUNT(*) as count FROM commands
1169
+ GROUP BY command ORDER BY count DESC LIMIT 1
1170
+ `).get();
1171
+ const mostUsedCommand = mostUsedRow?.command || null;
1172
+ const mostUsedCommandCount = mostUsedRow?.count || 0;
1173
+ const uniqueCommands = this.db.prepare("SELECT COUNT(DISTINCT command) as c FROM commands").get().c;
1174
+ const longestCommandLength = this.db.prepare("SELECT COALESCE(MAX(LENGTH(command)), 0) as c FROM commands").get().c;
1175
+ const peakHourRow = this.db.prepare(`
1176
+ SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as count
1177
+ FROM commands GROUP BY hour ORDER BY count DESC LIMIT 1
1178
+ `).get();
1179
+ const peakHour = peakHourRow?.hour ?? null;
1180
+ const peakHourCount = peakHourRow?.count ?? 0;
1181
+ const peakDayRow = this.db.prepare(`
1182
+ SELECT CAST(strftime('%w', timestamp) AS INTEGER) as day, COUNT(*) as count
1183
+ FROM commands GROUP BY day ORDER BY count DESC LIMIT 1
1184
+ `).get();
1185
+ const peakDay = peakDayRow?.day ?? null;
1186
+ const busiestDayRow = this.db.prepare(`
1187
+ SELECT DATE(timestamp) as day, COUNT(*) as count
1188
+ FROM commands GROUP BY day ORDER BY count DESC LIMIT 1
1189
+ `).get();
1190
+ const busiestDay = busiestDayRow?.day || null;
1191
+ const busiestDayCount = busiestDayRow?.count || 0;
1192
+ const avgCommandsPerSession = totalSessions > 0 ? Math.round(totalCommands / totalSessions) : 0;
1193
+ const longestSessionRow = this.db.prepare(`
1194
+ SELECT MAX(
1195
+ (julianday(COALESCE(end_time, datetime('now'))) - julianday(start_time)) * 1440
1196
+ ) as minutes FROM sessions
1197
+ `).get();
1198
+ const longestSessionMinutes = Math.round(longestSessionRow.minutes || 0);
1199
+ const lateNightCount = this.db.prepare(`
1200
+ SELECT COUNT(*) as c FROM commands
1201
+ WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5
1202
+ `).get().c;
1203
+ const avgRiskRow = this.db.prepare("SELECT AVG(risk_score) as avg FROM commands").get();
1204
+ const lifetimeAvgRisk = avgRiskRow.avg ?? 0;
1205
+ const allAllowed = this.db.prepare(
1206
+ "SELECT allowed FROM commands ORDER BY timestamp ASC"
1207
+ ).all();
1208
+ let cleanestStreak = 0;
1209
+ let currentStreak = 0;
1210
+ for (const row of allAllowed) {
1211
+ if (row.allowed === 1) {
1212
+ currentStreak++;
1213
+ if (currentStreak > cleanestStreak) cleanestStreak = currentStreak;
1214
+ } else {
1215
+ currentStreak = 0;
1216
+ }
1217
+ }
1218
+ const highestRiskRow = this.db.prepare(
1219
+ "SELECT command, risk_score FROM commands ORDER BY risk_score DESC LIMIT 1"
1220
+ ).get();
1221
+ const highestRiskCommand = highestRiskRow?.command || null;
1222
+ const highestRiskScore = highestRiskRow?.risk_score || 0;
1223
+ const highRiskCount = this.db.prepare(
1224
+ "SELECT COUNT(*) as c FROM commands WHERE risk_score >= 8"
1225
+ ).get().c;
1226
+ return {
1227
+ totalCommands,
1228
+ totalBlocked,
1229
+ totalSessions,
1230
+ totalCharacters,
1231
+ totalWatchTimeMinutes,
1232
+ uniqueRepos,
1233
+ memberSince,
1234
+ agentBreakdown,
1235
+ uniqueAgents,
1236
+ maxAgentCommands,
1237
+ favoriteAgent,
1238
+ agentsWith100,
1239
+ mostUsedCommand,
1240
+ mostUsedCommandCount,
1241
+ uniqueCommands,
1242
+ longestCommandLength,
1243
+ peakHour,
1244
+ peakHourCount,
1245
+ peakDay,
1246
+ busiestDay,
1247
+ busiestDayCount,
1248
+ avgCommandsPerSession,
1249
+ longestSessionMinutes,
1250
+ lateNightCount,
1251
+ lifetimeAvgRisk,
1252
+ cleanestStreak,
1253
+ currentCleanStreak: currentStreak,
1254
+ highestRiskCommand,
1255
+ highestRiskScore,
1256
+ highRiskCount
1257
+ };
1258
+ }
1259
+ computeAchievements(stats) {
1260
+ return _DashboardDB.BADGE_DEFINITIONS.map((badge) => {
1261
+ const value = stats[badge.stat] ?? 0;
1262
+ let currentTier = 0;
1263
+ for (let i = 0; i < badge.tiers.length; i++) {
1264
+ if (value >= badge.tiers[i]) currentTier = i + 1;
1265
+ }
1266
+ const nextThreshold = currentTier < badge.tiers.length ? badge.tiers[currentTier] : badge.tiers[badge.tiers.length - 1];
1267
+ const prevThreshold = currentTier > 0 ? badge.tiers[currentTier - 1] : 0;
1268
+ const progress = currentTier >= badge.tiers.length ? 1 : (value - prevThreshold) / Math.max(1, nextThreshold - prevThreshold);
1269
+ return {
1270
+ id: badge.id,
1271
+ name: badge.name,
1272
+ description: badge.description,
1273
+ category: badge.category,
1274
+ icon: badge.icon,
1275
+ tier: currentTier,
1276
+ tierName: TIER_NAMES[currentTier],
1277
+ value,
1278
+ nextThreshold,
1279
+ progress: Math.min(1, Math.max(0, progress)),
1280
+ maxed: currentTier >= 5
1281
+ };
1282
+ });
1283
+ }
1284
+ computeXP(stats, badges) {
1285
+ const TIER_XP = [0, 50, 100, 200, 500, 1e3];
1286
+ const RANK_THRESHOLDS = [
1287
+ { rank: "Obsidian", xp: 1e5 },
1288
+ { rank: "Diamond", xp: 25e3 },
1289
+ { rank: "Gold", xp: 5e3 },
1290
+ { rank: "Silver", xp: 1e3 },
1291
+ { rank: "Bronze", xp: 0 }
1292
+ ];
1293
+ let totalXP = 0;
1294
+ totalXP += stats.totalCommands;
1295
+ totalXP += stats.totalBlocked * 3;
1296
+ totalXP += stats.totalSessions * 10;
1297
+ totalXP += stats.lateNightCount * 2;
1298
+ totalXP += Math.floor(stats.cleanestStreak / 100) * 25;
1299
+ for (const badge of badges) {
1300
+ if (badge.tier > 0) {
1301
+ totalXP += TIER_XP[badge.tier];
1302
+ }
1303
+ }
1304
+ let rank = "Bronze";
1305
+ let nextRankXP = 1e3;
1306
+ for (const t of RANK_THRESHOLDS) {
1307
+ if (totalXP >= t.xp) {
1308
+ rank = t.rank;
1309
+ const idx = RANK_THRESHOLDS.indexOf(t);
1310
+ nextRankXP = idx > 0 ? RANK_THRESHOLDS[idx - 1].xp : totalXP;
1311
+ break;
1312
+ }
1313
+ }
1314
+ const currentRankXP = RANK_THRESHOLDS.find((t) => t.rank === rank).xp;
1315
+ const progress = rank === "Obsidian" ? 1 : (totalXP - currentRankXP) / Math.max(1, nextRankXP - currentRankXP);
1316
+ return {
1317
+ totalXP,
1318
+ rank,
1319
+ nextRankXP,
1320
+ progress: Math.min(1, Math.max(0, progress))
1321
+ };
755
1322
  }
756
1323
  close() {
757
1324
  this.db.close();
758
1325
  }
759
1326
  };
1327
+ var TIER_NAMES = {
1328
+ 0: "Locked",
1329
+ 1: "Bronze",
1330
+ 2: "Silver",
1331
+ 3: "Gold",
1332
+ 4: "Diamond",
1333
+ 5: "Obsidian"
1334
+ };
760
1335
  var db_default = DashboardDB;
761
1336
 
762
1337
  export {
763
1338
  DashboardDB,
764
1339
  db_default
765
1340
  };
766
- //# sourceMappingURL=chunk-YUMNBQAY.js.map
1341
+ //# sourceMappingURL=chunk-RDNSS3ME.js.map