@aurora-foundation/obsidian-next 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +86 -12
  2. package/dist/index.js +1497 -419
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import React7 from "react";
4
+ import React8 from "react";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/ui/Root.tsx
8
- import { useState as useState4, useEffect, useCallback as useCallback3 } from "react";
9
- import { Box as Box7, Text as Text7, useApp, useInput as useInput3 } from "ink";
8
+ import { useState as useState5, useEffect as useEffect2, useCallback as useCallback4 } from "react";
9
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput4 } from "ink";
10
10
  import TextInput from "ink-text-input";
11
11
 
12
12
  // src/core/bus.ts
@@ -423,10 +423,432 @@ var ChoicePrompt = ({
423
423
  );
424
424
  };
425
425
 
426
- // src/ui/Dashboard.tsx
427
- import React5 from "react";
428
- import { Box as Box5, Text as Text5 } from "ink";
426
+ // src/components/SettingsMenu.tsx
427
+ import { useState as useState4, useCallback as useCallback3, useEffect } from "react";
428
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
429
+
430
+ // src/core/settings.ts
431
+ import fs from "fs/promises";
432
+ import path from "path";
433
+ import { z } from "zod";
434
+ var SETTINGS_DIR = ".obsidian";
435
+ var SETTINGS_FILE = "settings.json";
436
+ var SettingsSchema = z.object({
437
+ // Execution mode
438
+ mode: z.enum(["auto", "plan", "safe"]).default("safe"),
439
+ // Auto-accept settings
440
+ autoAccept: z.object({
441
+ enabled: z.boolean().default(false),
442
+ readOperations: z.boolean().default(true),
443
+ safeCommands: z.boolean().default(true)
444
+ }).default({}),
445
+ // Tool permissions
446
+ permissions: z.object({
447
+ // Patterns always allowed without prompt: "tool:pattern"
448
+ allow: z.array(z.string()).default([]),
449
+ // Patterns always blocked
450
+ deny: z.array(z.string()).default([])
451
+ }).default({}),
452
+ // Security settings
453
+ security: z.object({
454
+ // PII redaction before sending to LLM
455
+ piiRedaction: z.boolean().default(true),
456
+ // Audit logging of all commands
457
+ auditLogging: z.boolean().default(true),
458
+ // Key storage backend preference
459
+ keyBackend: z.enum(["auto", "keychain", "secret-tool", "encrypted-file", "env"]).default("auto")
460
+ }).default({}),
461
+ // UI preferences
462
+ ui: z.object({
463
+ syntaxHighlight: z.boolean().default(true),
464
+ diffColors: z.boolean().default(true),
465
+ showLineNumbers: z.boolean().default(true)
466
+ }).default({})
467
+ });
468
+ var DEFAULT_SETTINGS = {
469
+ mode: "safe",
470
+ autoAccept: {
471
+ enabled: false,
472
+ readOperations: false,
473
+ safeCommands: false
474
+ },
475
+ permissions: {
476
+ allow: [],
477
+ // Empty - user builds their own allow list
478
+ deny: []
479
+ },
480
+ security: {
481
+ piiRedaction: true,
482
+ // Enabled by default - protects user privacy
483
+ auditLogging: true,
484
+ // Enabled by default - for accountability
485
+ keyBackend: "auto"
486
+ // Auto-detect best available backend
487
+ },
488
+ ui: {
489
+ syntaxHighlight: true,
490
+ diffColors: true,
491
+ showLineNumbers: true
492
+ }
493
+ };
494
+ var SettingsManager = class {
495
+ settingsPath;
496
+ cached = null;
497
+ constructor() {
498
+ this.settingsPath = path.join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE);
499
+ }
500
+ async load() {
501
+ if (this.cached) return this.cached;
502
+ return this.reload();
503
+ }
504
+ async reload() {
505
+ try {
506
+ const data = await fs.readFile(this.settingsPath, "utf-8");
507
+ const parsed = JSON.parse(data);
508
+ this.cached = SettingsSchema.parse({ ...DEFAULT_SETTINGS, ...parsed });
509
+ } catch {
510
+ this.cached = DEFAULT_SETTINGS;
511
+ await this.save(DEFAULT_SETTINGS);
512
+ }
513
+ return this.cached;
514
+ }
515
+ /**
516
+ * Add a permission to the allow list (called when user approves a command)
517
+ */
518
+ async addAllowedPermission(tool, command) {
519
+ const s = await this.load();
520
+ const pattern = `${tool}:${command}`;
521
+ if (!s.permissions.allow.includes(pattern)) {
522
+ s.permissions.allow.push(pattern);
523
+ await this.save({ permissions: s.permissions });
524
+ }
525
+ }
526
+ /**
527
+ * Add a permission to the deny list
528
+ */
529
+ async addDeniedPermission(tool, command) {
530
+ const s = await this.load();
531
+ const pattern = `${tool}:${command}`;
532
+ if (!s.permissions.deny.includes(pattern)) {
533
+ s.permissions.deny.push(pattern);
534
+ await this.save({ permissions: s.permissions });
535
+ }
536
+ }
537
+ async save(newSettings) {
538
+ const current = await this.load();
539
+ const merged = { ...current, ...newSettings };
540
+ if (newSettings.autoAccept) {
541
+ merged.autoAccept = { ...current.autoAccept, ...newSettings.autoAccept };
542
+ }
543
+ if (newSettings.permissions) {
544
+ merged.permissions = { ...current.permissions, ...newSettings.permissions };
545
+ }
546
+ if (newSettings.security) {
547
+ merged.security = { ...current.security, ...newSettings.security };
548
+ }
549
+ if (newSettings.ui) {
550
+ merged.ui = { ...current.ui, ...newSettings.ui };
551
+ }
552
+ const validated = SettingsSchema.parse(merged);
553
+ const dir = path.dirname(this.settingsPath);
554
+ await fs.mkdir(dir, { recursive: true });
555
+ await fs.writeFile(this.settingsPath, JSON.stringify(validated, null, 2));
556
+ this.cached = validated;
557
+ }
558
+ async get(key) {
559
+ const s = await this.load();
560
+ return s[key];
561
+ }
562
+ async set(key, value) {
563
+ await this.save({ [key]: value });
564
+ }
565
+ /**
566
+ * Check if a tool:command pattern is allowed
567
+ */
568
+ async isAllowed(tool, command) {
569
+ const s = await this.load();
570
+ const pattern = `${tool}:${command}`;
571
+ for (const deny of s.permissions.deny) {
572
+ if (this.matchPattern(pattern, deny)) {
573
+ return false;
574
+ }
575
+ }
576
+ for (const allow of s.permissions.allow) {
577
+ if (this.matchPattern(pattern, allow)) {
578
+ return true;
579
+ }
580
+ }
581
+ return false;
582
+ }
583
+ /**
584
+ * Check if a tool:command pattern is explicitly denied
585
+ */
586
+ async isDenied(tool, command) {
587
+ const s = await this.load();
588
+ const pattern = `${tool}:${command}`;
589
+ for (const deny of s.permissions.deny) {
590
+ if (this.matchPattern(pattern, deny)) {
591
+ return true;
592
+ }
593
+ }
594
+ return false;
595
+ }
596
+ /**
597
+ * Simple glob-like pattern matching
598
+ * Supports * as wildcard
599
+ */
600
+ matchPattern(value, pattern) {
601
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
602
+ return new RegExp(`^${regex}$`).test(value);
603
+ }
604
+ clearCache() {
605
+ this.cached = null;
606
+ }
607
+ getPath() {
608
+ return this.settingsPath;
609
+ }
610
+ };
611
+ var settings = new SettingsManager();
612
+
613
+ // src/components/SettingsMenu.tsx
429
614
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
615
+ var SettingsMenu = ({ onClose }) => {
616
+ const [view, setView] = useState4("categories");
617
+ const [selectedIndex, setSelectedIndex] = useState4(0);
618
+ const [currentSettings, setCurrentSettings] = useState4(null);
619
+ const [saving, setSaving] = useState4(false);
620
+ useEffect(() => {
621
+ settings.load().then(setCurrentSettings);
622
+ }, []);
623
+ useEffect(() => {
624
+ setSelectedIndex(0);
625
+ }, [view]);
626
+ const saveAndUpdate = useCallback3(async (updates) => {
627
+ setSaving(true);
628
+ await settings.save(updates);
629
+ const updated = await settings.reload();
630
+ setCurrentSettings(updated);
631
+ setSaving(false);
632
+ }, []);
633
+ const getMenuItems = useCallback3(() => {
634
+ if (!currentSettings) return [];
635
+ switch (view) {
636
+ case "categories":
637
+ return [
638
+ { key: "mode", label: "Execution Mode", type: "category", description: `Current: ${currentSettings.mode}` },
639
+ { key: "security", label: "Security", type: "category", description: "PII redaction, audit logging" },
640
+ { key: "ui", label: "UI Preferences", type: "category", description: "Syntax highlighting, colors" },
641
+ { key: "permissions", label: "Permissions", type: "category", description: "Allow/deny lists" },
642
+ { key: "close", label: "Close Settings", type: "action" }
643
+ ];
644
+ case "mode":
645
+ return [
646
+ { key: "auto", label: "Auto Mode", type: "select", value: currentSettings.mode === "auto", description: "Execute all commands without confirmation" },
647
+ { key: "plan", label: "Plan Mode", type: "select", value: currentSettings.mode === "plan", description: "Read-only planning, approve before execution" },
648
+ { key: "safe", label: "Safe Mode", type: "select", value: currentSettings.mode === "safe", description: "Require approval for all write operations" },
649
+ { key: "back", label: "Back", type: "action" }
650
+ ];
651
+ case "security":
652
+ return [
653
+ { key: "piiRedaction", label: "PII Redaction", type: "toggle", value: currentSettings.security.piiRedaction, description: "Redact sensitive data before sending to AI" },
654
+ { key: "auditLogging", label: "Audit Logging", type: "toggle", value: currentSettings.security.auditLogging, description: "Log all commands and file operations" },
655
+ { key: "keyBackend", label: "Key Storage", type: "category", description: `Current: ${currentSettings.security.keyBackend}` },
656
+ { key: "back", label: "Back", type: "action" }
657
+ ];
658
+ case "ui":
659
+ return [
660
+ { key: "syntaxHighlight", label: "Syntax Highlighting", type: "toggle", value: currentSettings.ui.syntaxHighlight, description: "Colorize code output" },
661
+ { key: "diffColors", label: "Diff Colors", type: "toggle", value: currentSettings.ui.diffColors, description: "Show colored diffs" },
662
+ { key: "showLineNumbers", label: "Line Numbers", type: "toggle", value: currentSettings.ui.showLineNumbers, description: "Show line numbers in file output" },
663
+ { key: "back", label: "Back", type: "action" }
664
+ ];
665
+ case "permissions":
666
+ const allowCount = currentSettings.permissions.allow.length;
667
+ const denyCount = currentSettings.permissions.deny.length;
668
+ return [
669
+ { key: "viewAllow", label: "View Allowed Patterns", type: "category", description: `${allowCount} pattern(s)` },
670
+ { key: "viewDeny", label: "View Denied Patterns", type: "category", description: `${denyCount} pattern(s)` },
671
+ { key: "clearAllow", label: "Clear Allowed Patterns", type: "action", description: "Reset allow list" },
672
+ { key: "clearDeny", label: "Clear Denied Patterns", type: "action", description: "Reset deny list" },
673
+ { key: "back", label: "Back", type: "action" }
674
+ ];
675
+ default:
676
+ return [];
677
+ }
678
+ }, [view, currentSettings]);
679
+ const items = getMenuItems();
680
+ const handleSelect = useCallback3(async () => {
681
+ const item = items[selectedIndex];
682
+ if (!item || !currentSettings) return;
683
+ switch (item.type) {
684
+ case "category":
685
+ if (item.key === "keyBackend") {
686
+ const backends = ["auto", "keychain", "secret-tool", "encrypted-file", "env"];
687
+ const currentIdx = backends.indexOf(currentSettings.security.keyBackend);
688
+ const nextBackend = backends[(currentIdx + 1) % backends.length];
689
+ await saveAndUpdate({ security: { ...currentSettings.security, keyBackend: nextBackend } });
690
+ } else if (item.key === "viewAllow") {
691
+ } else if (item.key === "viewDeny") {
692
+ } else {
693
+ setView(item.key);
694
+ }
695
+ break;
696
+ case "toggle":
697
+ if (view === "security") {
698
+ await saveAndUpdate({
699
+ security: {
700
+ ...currentSettings.security,
701
+ [item.key]: !item.value
702
+ }
703
+ });
704
+ } else if (view === "ui") {
705
+ await saveAndUpdate({
706
+ ui: {
707
+ ...currentSettings.ui,
708
+ [item.key]: !item.value
709
+ }
710
+ });
711
+ }
712
+ break;
713
+ case "select":
714
+ if (view === "mode" && item.key !== "back") {
715
+ await saveAndUpdate({ mode: item.key });
716
+ }
717
+ break;
718
+ case "action":
719
+ if (item.key === "back") {
720
+ setView("categories");
721
+ } else if (item.key === "close") {
722
+ onClose();
723
+ } else if (item.key === "clearAllow") {
724
+ await saveAndUpdate({ permissions: { ...currentSettings.permissions, allow: [] } });
725
+ } else if (item.key === "clearDeny") {
726
+ await saveAndUpdate({ permissions: { ...currentSettings.permissions, deny: [] } });
727
+ }
728
+ break;
729
+ }
730
+ }, [items, selectedIndex, currentSettings, view, saveAndUpdate, onClose]);
731
+ useInput3((input, key) => {
732
+ if (key.upArrow) {
733
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : items.length - 1);
734
+ }
735
+ if (key.downArrow) {
736
+ setSelectedIndex((prev) => prev < items.length - 1 ? prev + 1 : 0);
737
+ }
738
+ if (key.return) {
739
+ handleSelect();
740
+ }
741
+ if (key.escape) {
742
+ if (view === "categories") {
743
+ onClose();
744
+ } else {
745
+ setView("categories");
746
+ }
747
+ }
748
+ const num = parseInt(input, 10);
749
+ if (num >= 1 && num <= items.length) {
750
+ setSelectedIndex(num - 1);
751
+ }
752
+ });
753
+ if (!currentSettings) {
754
+ return /* @__PURE__ */ jsx5(Box5, { borderStyle: "round", borderColor: "cyan", padding: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Loading settings..." }) });
755
+ }
756
+ const getViewTitle = () => {
757
+ switch (view) {
758
+ case "categories":
759
+ return "Settings";
760
+ case "mode":
761
+ return "Execution Mode";
762
+ case "security":
763
+ return "Security Settings";
764
+ case "ui":
765
+ return "UI Preferences";
766
+ case "permissions":
767
+ return "Permission Lists";
768
+ default:
769
+ return "Settings";
770
+ }
771
+ };
772
+ return /* @__PURE__ */ jsxs5(
773
+ Box5,
774
+ {
775
+ flexDirection: "column",
776
+ borderStyle: "round",
777
+ borderColor: "cyan",
778
+ paddingX: 1,
779
+ paddingY: 0,
780
+ marginY: 1,
781
+ children: [
782
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", children: [
783
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "cyan", children: [
784
+ "[*] ",
785
+ getViewTitle()
786
+ ] }),
787
+ saving && /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Saving..." })
788
+ ] }),
789
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: items.map((item, index) => {
790
+ const isSelected = index === selectedIndex;
791
+ const indicator = isSelected ? ">" : " ";
792
+ let valueDisplay = null;
793
+ if (item.type === "toggle") {
794
+ valueDisplay = /* @__PURE__ */ jsxs5(Text5, { color: item.value ? "green" : "red", children: [
795
+ "[",
796
+ item.value ? "ON" : "OFF",
797
+ "]"
798
+ ] });
799
+ } else if (item.type === "select" && view === "mode") {
800
+ valueDisplay = /* @__PURE__ */ jsx5(Text5, { color: item.value ? "green" : "gray", children: item.value ? "[*]" : "[ ]" });
801
+ }
802
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
803
+ /* @__PURE__ */ jsxs5(Box5, { children: [
804
+ /* @__PURE__ */ jsxs5(Text5, { color: isSelected ? "cyan" : "gray", children: [
805
+ indicator,
806
+ " "
807
+ ] }),
808
+ /* @__PURE__ */ jsxs5(Text5, { color: isSelected ? "white" : "gray", bold: isSelected, children: [
809
+ "[",
810
+ index + 1,
811
+ "] ",
812
+ item.label
813
+ ] }),
814
+ valueDisplay && /* @__PURE__ */ jsx5(Text5, { children: " " }),
815
+ valueDisplay
816
+ ] }),
817
+ item.description && isSelected && /* @__PURE__ */ jsx5(Box5, { marginLeft: 4, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: item.description }) })
818
+ ] }, item.key);
819
+ }) }),
820
+ view === "permissions" && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
821
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", bold: true, children: "Allowed patterns:" }),
822
+ currentSettings.permissions.allow.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: " (none)" }) : currentSettings.permissions.allow.slice(0, 5).map((p, i) => /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
823
+ " + ",
824
+ p
825
+ ] }, i)),
826
+ currentSettings.permissions.allow.length > 5 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
827
+ " ... and ",
828
+ currentSettings.permissions.allow.length - 5,
829
+ " more"
830
+ ] }),
831
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", bold: true, children: "Denied patterns:" }),
832
+ currentSettings.permissions.deny.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: " (none)" }) : currentSettings.permissions.deny.slice(0, 5).map((p, i) => /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
833
+ " - ",
834
+ p
835
+ ] }, i)),
836
+ currentSettings.permissions.deny.length > 5 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
837
+ " ... and ",
838
+ currentSettings.permissions.deny.length - 5,
839
+ " more"
840
+ ] })
841
+ ] }),
842
+ /* @__PURE__ */ jsx5(Box5, { borderStyle: "single", borderColor: "gray", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingTop: 0, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Arrows: navigate | Enter: select/toggle | Esc: back | 1-9: quick select" }) })
843
+ ]
844
+ }
845
+ );
846
+ };
847
+
848
+ // src/ui/Dashboard.tsx
849
+ import React6 from "react";
850
+ import { Box as Box6, Text as Text6 } from "ink";
851
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
430
852
  var flareAnim2 = ["\xB7", "\u25AA", "\u259A", "\u2756", "\u2726", "\u2739", "\u2726", "\u25AA"];
431
853
  var owlSprites = {
432
854
  idle: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
@@ -459,23 +881,23 @@ var Dashboard = ({
459
881
  model = "Claude Sonnet 4.5",
460
882
  workspace = process.cwd()
461
883
  }) => {
462
- const [flareFrame, setFlareFrame] = React5.useState(0);
463
- const [owlState, setOwlState] = React5.useState("idle");
464
- const [columns, setColumns] = React5.useState(process.stdout.columns);
465
- React5.useEffect(() => {
884
+ const [flareFrame, setFlareFrame] = React6.useState(0);
885
+ const [owlState, setOwlState] = React6.useState("idle");
886
+ const [columns, setColumns] = React6.useState(process.stdout.columns);
887
+ React6.useEffect(() => {
466
888
  const onResize = () => setColumns(process.stdout.columns);
467
889
  process.stdout.on("resize", onResize);
468
890
  return () => {
469
891
  process.stdout.off("resize", onResize);
470
892
  };
471
893
  }, []);
472
- React5.useEffect(() => {
894
+ React6.useEffect(() => {
473
895
  const interval = setInterval(() => {
474
896
  setFlareFrame((prev) => (prev + 1) % flareAnim2.length);
475
897
  }, 100);
476
898
  return () => clearInterval(interval);
477
899
  }, []);
478
- React5.useEffect(() => {
900
+ React6.useEffect(() => {
479
901
  let isActive = true;
480
902
  const loop = async () => {
481
903
  while (isActive) {
@@ -497,8 +919,8 @@ var Dashboard = ({
497
919
  };
498
920
  }, []);
499
921
  const showRightColumn = columns >= 100;
500
- return /* @__PURE__ */ jsxs5(
501
- Box5,
922
+ return /* @__PURE__ */ jsxs6(
923
+ Box6,
502
924
  {
503
925
  borderStyle: "round",
504
926
  borderColor: "red",
@@ -506,45 +928,45 @@ var Dashboard = ({
506
928
  paddingX: 1,
507
929
  paddingY: 0,
508
930
  children: [
509
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width: showRightColumn ? "60%" : "100%", paddingRight: showRightColumn ? 1 : 0, children: [
510
- /* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "white", children: [
931
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width: showRightColumn ? "60%" : "100%", paddingRight: showRightColumn ? 1 : 0, children: [
932
+ /* @__PURE__ */ jsx6(Box6, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "white", children: [
511
933
  "Welcome back, ",
512
934
  username,
513
935
  "!"
514
936
  ] }) }),
515
- /* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: owlSprites[owlState] }) }),
516
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", alignItems: "center", children: [
517
- /* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
937
+ /* @__PURE__ */ jsx6(Box6, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: owlSprites[owlState] }) }),
938
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", alignItems: "center", children: [
939
+ /* @__PURE__ */ jsxs6(Text6, { color: "white", children: [
518
940
  model,
519
941
  " ",
520
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: flareAnim2[flareFrame] }),
942
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: flareAnim2[flareFrame] }),
521
943
  " Obsidian Next"
522
944
  ] }),
523
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: workspace })
945
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: workspace })
524
946
  ] })
525
947
  ] }),
526
- showRightColumn && /* @__PURE__ */ jsx5(Box5, { borderStyle: "single", borderTop: false, borderBottom: false, borderLeft: false, borderColor: "red", marginX: 1 }),
527
- showRightColumn && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width: "40%", paddingLeft: 1, children: [
528
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
529
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "Commands" }),
530
- /* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
531
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "/help" }),
948
+ showRightColumn && /* @__PURE__ */ jsx6(Box6, { borderStyle: "single", borderTop: false, borderBottom: false, borderLeft: false, borderColor: "red", marginX: 1 }),
949
+ showRightColumn && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width: "40%", paddingLeft: 1, children: [
950
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 1, children: [
951
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "Commands" }),
952
+ /* @__PURE__ */ jsxs6(Text6, { color: "white", children: [
953
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "/help" }),
532
954
  " Show all commands"
533
955
  ] }),
534
- /* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
535
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "/tool" }),
956
+ /* @__PURE__ */ jsxs6(Text6, { color: "white", children: [
957
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "/tool" }),
536
958
  " Execute tools"
537
959
  ] }),
538
- /* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
539
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "/clear" }),
960
+ /* @__PURE__ */ jsxs6(Text6, { color: "white", children: [
961
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "/clear" }),
540
962
  " Clear history"
541
963
  ] })
542
964
  ] }),
543
- /* @__PURE__ */ jsx5(Box5, { borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderColor: "red", marginBottom: 1 }),
544
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
545
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "Quick Start" }),
546
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Ask me to read, edit, or" }),
547
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "run commands in your code." })
965
+ /* @__PURE__ */ jsx6(Box6, { borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderColor: "red", marginBottom: 1 }),
966
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
967
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "Quick Start" }),
968
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Ask me to read, edit, or" }),
969
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "run commands in your code." })
548
970
  ] })
549
971
  ] })
550
972
  ]
@@ -553,8 +975,8 @@ var Dashboard = ({
553
975
  };
554
976
 
555
977
  // src/ui/CommandPopup.tsx
556
- import { Box as Box6, Text as Text6 } from "ink";
557
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
978
+ import { Box as Box7, Text as Text7 } from "ink";
979
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
558
980
  var COMMANDS = [
559
981
  { name: "/help", desc: "Show available commands" },
560
982
  { name: "/init", desc: "Initialize configuration" },
@@ -575,8 +997,8 @@ var COMMANDS = [
575
997
  ];
576
998
  var CommandPopup = ({ matches, selectedIndex }) => {
577
999
  if (matches.length === 0) return null;
578
- return /* @__PURE__ */ jsx6(
579
- Box6,
1000
+ return /* @__PURE__ */ jsx7(
1001
+ Box7,
580
1002
  {
581
1003
  flexDirection: "column",
582
1004
  borderStyle: "round",
@@ -586,12 +1008,12 @@ var CommandPopup = ({ matches, selectedIndex }) => {
586
1008
  width: "100%",
587
1009
  children: matches.map((cmd, i) => {
588
1010
  const isSelected = i === selectedIndex;
589
- return /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
590
- /* @__PURE__ */ jsxs6(Text6, { color: isSelected ? "cyan" : "red", bold: isSelected, children: [
1011
+ return /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
1012
+ /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? "cyan" : "red", bold: isSelected, children: [
591
1013
  isSelected ? "> " : " ",
592
1014
  cmd.name
593
1015
  ] }),
594
- /* @__PURE__ */ jsx6(Text6, { color: isSelected ? "white" : "gray", children: cmd.desc })
1016
+ /* @__PURE__ */ jsx7(Text7, { color: isSelected ? "white" : "gray", children: cmd.desc })
595
1017
  ] }, cmd.name);
596
1018
  })
597
1019
  }
@@ -599,18 +1021,18 @@ var CommandPopup = ({ matches, selectedIndex }) => {
599
1021
  };
600
1022
 
601
1023
  // src/core/history.ts
602
- import fs from "fs/promises";
603
- import path from "path";
1024
+ import fs2 from "fs/promises";
1025
+ import path2 from "path";
604
1026
  import os from "os";
605
1027
  var HistoryManager = class {
606
1028
  historyPath;
607
1029
  saveTimer = null;
608
1030
  constructor(customPath) {
609
- this.historyPath = customPath || path.join(os.homedir(), ".obsidian", "history.json");
1031
+ this.historyPath = customPath || path2.join(os.homedir(), ".obsidian", "history.json");
610
1032
  }
611
1033
  async load() {
612
1034
  try {
613
- const data = await fs.readFile(this.historyPath, "utf-8");
1035
+ const data = await fs2.readFile(this.historyPath, "utf-8");
614
1036
  const events = JSON.parse(data);
615
1037
  return Array.isArray(events) ? events : [];
616
1038
  } catch {
@@ -621,9 +1043,9 @@ var HistoryManager = class {
621
1043
  if (this.saveTimer) clearTimeout(this.saveTimer);
622
1044
  this.saveTimer = setTimeout(async () => {
623
1045
  try {
624
- const dir = path.dirname(this.historyPath);
625
- await fs.mkdir(dir, { recursive: true });
626
- await fs.writeFile(this.historyPath, JSON.stringify(events, null, 2));
1046
+ const dir = path2.dirname(this.historyPath);
1047
+ await fs2.mkdir(dir, { recursive: true });
1048
+ await fs2.writeFile(this.historyPath, JSON.stringify(events, null, 2));
627
1049
  } catch (error) {
628
1050
  console.error("Failed to save history:", error);
629
1051
  }
@@ -632,7 +1054,7 @@ var HistoryManager = class {
632
1054
  async clear() {
633
1055
  if (this.saveTimer) clearTimeout(this.saveTimer);
634
1056
  try {
635
- await fs.writeFile(this.historyPath, JSON.stringify([], null, 2));
1057
+ await fs2.writeFile(this.historyPath, JSON.stringify([], null, 2));
636
1058
  } catch {
637
1059
  }
638
1060
  }
@@ -640,16 +1062,16 @@ var HistoryManager = class {
640
1062
  var history = new HistoryManager();
641
1063
 
642
1064
  // src/core/usage.ts
643
- import fs2 from "fs/promises";
644
- import path2 from "path";
1065
+ import fs3 from "fs/promises";
1066
+ import path3 from "path";
645
1067
  import os2 from "os";
646
- import { z } from "zod";
647
- var UsageSchema = z.object({
648
- totalSessions: z.number().default(0),
649
- totalRequests: z.number().default(0),
650
- totalInputTokens: z.number().default(0),
651
- totalOutputTokens: z.number().default(0),
652
- totalCost: z.number().default(0)
1068
+ import { z as z2 } from "zod";
1069
+ var UsageSchema = z2.object({
1070
+ totalSessions: z2.number().default(0),
1071
+ totalRequests: z2.number().default(0),
1072
+ totalInputTokens: z2.number().default(0),
1073
+ totalOutputTokens: z2.number().default(0),
1074
+ totalCost: z2.number().default(0)
653
1075
  });
654
1076
  var MODEL_PRICES = {
655
1077
  // Claude 4.5 Family (2025-2026)
@@ -668,12 +1090,12 @@ var UsageTracker = class {
668
1090
  stats;
669
1091
  sessionCost = 0;
670
1092
  constructor(customPath) {
671
- this.usagePath = customPath || path2.join(os2.homedir(), ".obsidian", "usage.json");
1093
+ this.usagePath = customPath || path3.join(os2.homedir(), ".obsidian", "usage.json");
672
1094
  this.stats = UsageSchema.parse({});
673
1095
  }
674
1096
  async init() {
675
1097
  try {
676
- const data = await fs2.readFile(this.usagePath, "utf-8");
1098
+ const data = await fs3.readFile(this.usagePath, "utf-8");
677
1099
  this.stats = UsageSchema.parse(JSON.parse(data));
678
1100
  } catch {
679
1101
  await this.save();
@@ -704,250 +1126,85 @@ var UsageTracker = class {
704
1126
  return this.stats;
705
1127
  }
706
1128
  async save() {
707
- const dir = path2.dirname(this.usagePath);
708
- await fs2.mkdir(dir, { recursive: true });
709
- await fs2.writeFile(this.usagePath, JSON.stringify(this.stats, null, 2));
1129
+ const dir = path3.dirname(this.usagePath);
1130
+ await fs3.mkdir(dir, { recursive: true });
1131
+ await fs3.writeFile(this.usagePath, JSON.stringify(this.stats, null, 2));
710
1132
  }
711
1133
  };
712
1134
  var usage = new UsageTracker();
713
1135
 
714
1136
  // src/core/config.ts
715
- import fs3 from "fs/promises";
716
- import path3 from "path";
717
- import os3 from "os";
718
- import { z as z2 } from "zod";
719
- import dotenv from "dotenv";
720
- dotenv.config();
721
- var ConfigSchema = z2.object({
722
- apiKey: z2.string().optional(),
723
- model: z2.string().default("claude-sonnet-4-5-20250929"),
724
- workspaceRoot: z2.string().default(process.cwd()),
725
- maxTokens: z2.number().default(8192),
726
- language: z2.string().default("en")
727
- });
728
- var DEFAULT_CONFIG = {
729
- model: "claude-3-5-sonnet",
730
- maxTokens: 4096,
731
- language: "en",
732
- workspaceRoot: process.cwd()
733
- };
734
- var ConfigManager = class {
735
- configPath;
736
- cachedConfig = null;
737
- constructor(customPath) {
738
- this.configPath = customPath || path3.join(os3.homedir(), ".obsidian", "config.json");
739
- }
740
- async load() {
741
- if (this.cachedConfig) return this.cachedConfig;
742
- return this.reload();
743
- }
744
- async reload() {
745
- let loadedConfig = DEFAULT_CONFIG;
746
- try {
747
- const data = await fs3.readFile(this.configPath, "utf-8");
748
- const parsed = JSON.parse(data);
749
- loadedConfig = { ...DEFAULT_CONFIG, ...parsed };
750
- } catch {
751
- }
752
- const envKey = process.env.ANTHROPIC_API_KEY;
753
- const finalConfig = ConfigSchema.parse({
754
- ...loadedConfig,
755
- apiKey: envKey || loadedConfig.apiKey
756
- });
757
- this.cachedConfig = finalConfig;
758
- return finalConfig;
759
- }
760
- clearCache() {
761
- this.cachedConfig = null;
762
- }
763
- async save(config2) {
764
- const dir = path3.dirname(this.configPath);
765
- await fs3.mkdir(dir, { recursive: true });
766
- await fs3.writeFile(this.configPath, JSON.stringify(config2, null, 2));
767
- this.clearCache();
768
- }
769
- async exists() {
770
- try {
771
- await fs3.access(this.configPath);
772
- return true;
773
- } catch {
774
- return false;
775
- }
776
- }
777
- getPath() {
778
- return this.configPath;
779
- }
780
- };
781
- var config = new ConfigManager();
782
-
783
- // src/core/context.ts
784
- import fs5 from "fs/promises";
785
- import path5 from "path";
786
-
787
- // src/core/settings.ts
788
1137
  import fs4 from "fs/promises";
789
1138
  import path4 from "path";
1139
+ import os3 from "os";
790
1140
  import { z as z3 } from "zod";
791
- var SETTINGS_DIR = ".obsidian";
792
- var SETTINGS_FILE = "settings.json";
793
- var SettingsSchema = z3.object({
794
- // Execution mode
795
- mode: z3.enum(["auto", "plan", "safe"]).default("safe"),
796
- // Auto-accept settings
797
- autoAccept: z3.object({
798
- enabled: z3.boolean().default(false),
799
- readOperations: z3.boolean().default(true),
800
- safeCommands: z3.boolean().default(true)
801
- }).default({}),
802
- // Tool permissions
803
- permissions: z3.object({
804
- // Patterns always allowed without prompt: "tool:pattern"
805
- allow: z3.array(z3.string()).default([]),
806
- // Patterns always blocked
807
- deny: z3.array(z3.string()).default([])
808
- }).default({}),
809
- // UI preferences
810
- ui: z3.object({
811
- syntaxHighlight: z3.boolean().default(true),
812
- diffColors: z3.boolean().default(true),
813
- showLineNumbers: z3.boolean().default(true)
814
- }).default({})
815
- });
816
- var DEFAULT_SETTINGS = {
817
- mode: "safe",
818
- autoAccept: {
819
- enabled: false,
820
- readOperations: false,
821
- safeCommands: false
822
- },
823
- permissions: {
824
- allow: [],
825
- // Empty - user builds their own allow list
826
- deny: []
827
- },
828
- ui: {
829
- syntaxHighlight: true,
830
- diffColors: true,
831
- showLineNumbers: true
832
- }
1141
+ import dotenv from "dotenv";
1142
+ dotenv.config();
1143
+ var ConfigSchema = z3.object({
1144
+ apiKey: z3.string().optional(),
1145
+ model: z3.string().default("claude-sonnet-4-5-20250929"),
1146
+ workspaceRoot: z3.string().default(process.cwd()),
1147
+ maxTokens: z3.number().default(8192),
1148
+ language: z3.string().default("en")
1149
+ });
1150
+ var DEFAULT_CONFIG = {
1151
+ model: "claude-3-5-sonnet",
1152
+ maxTokens: 4096,
1153
+ language: "en",
1154
+ workspaceRoot: process.cwd()
833
1155
  };
834
- var SettingsManager = class {
835
- settingsPath;
836
- cached = null;
837
- constructor() {
838
- this.settingsPath = path4.join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE);
1156
+ var ConfigManager = class {
1157
+ configPath;
1158
+ cachedConfig = null;
1159
+ constructor(customPath) {
1160
+ this.configPath = customPath || path4.join(os3.homedir(), ".obsidian", "config.json");
839
1161
  }
840
1162
  async load() {
841
- if (this.cached) return this.cached;
1163
+ if (this.cachedConfig) return this.cachedConfig;
842
1164
  return this.reload();
843
1165
  }
844
1166
  async reload() {
1167
+ let loadedConfig = DEFAULT_CONFIG;
845
1168
  try {
846
- const data = await fs4.readFile(this.settingsPath, "utf-8");
1169
+ const data = await fs4.readFile(this.configPath, "utf-8");
847
1170
  const parsed = JSON.parse(data);
848
- this.cached = SettingsSchema.parse({ ...DEFAULT_SETTINGS, ...parsed });
1171
+ loadedConfig = { ...DEFAULT_CONFIG, ...parsed };
849
1172
  } catch {
850
- this.cached = DEFAULT_SETTINGS;
851
- await this.save(DEFAULT_SETTINGS);
852
- }
853
- return this.cached;
854
- }
855
- /**
856
- * Add a permission to the allow list (called when user approves a command)
857
- */
858
- async addAllowedPermission(tool, command) {
859
- const s = await this.load();
860
- const pattern = `${tool}:${command}`;
861
- if (!s.permissions.allow.includes(pattern)) {
862
- s.permissions.allow.push(pattern);
863
- await this.save({ permissions: s.permissions });
864
1173
  }
1174
+ const envKey = process.env.ANTHROPIC_API_KEY;
1175
+ const finalConfig = ConfigSchema.parse({
1176
+ ...loadedConfig,
1177
+ apiKey: envKey || loadedConfig.apiKey
1178
+ });
1179
+ this.cachedConfig = finalConfig;
1180
+ return finalConfig;
865
1181
  }
866
- /**
867
- * Add a permission to the deny list
868
- */
869
- async addDeniedPermission(tool, command) {
870
- const s = await this.load();
871
- const pattern = `${tool}:${command}`;
872
- if (!s.permissions.deny.includes(pattern)) {
873
- s.permissions.deny.push(pattern);
874
- await this.save({ permissions: s.permissions });
875
- }
1182
+ clearCache() {
1183
+ this.cachedConfig = null;
876
1184
  }
877
- async save(newSettings) {
878
- const current = await this.load();
879
- const merged = { ...current, ...newSettings };
880
- if (newSettings.autoAccept) {
881
- merged.autoAccept = { ...current.autoAccept, ...newSettings.autoAccept };
882
- }
883
- if (newSettings.permissions) {
884
- merged.permissions = { ...current.permissions, ...newSettings.permissions };
885
- }
886
- if (newSettings.ui) {
887
- merged.ui = { ...current.ui, ...newSettings.ui };
888
- }
889
- const validated = SettingsSchema.parse(merged);
890
- const dir = path4.dirname(this.settingsPath);
1185
+ async save(config2) {
1186
+ const dir = path4.dirname(this.configPath);
891
1187
  await fs4.mkdir(dir, { recursive: true });
892
- await fs4.writeFile(this.settingsPath, JSON.stringify(validated, null, 2));
893
- this.cached = validated;
894
- }
895
- async get(key) {
896
- const s = await this.load();
897
- return s[key];
898
- }
899
- async set(key, value) {
900
- await this.save({ [key]: value });
901
- }
902
- /**
903
- * Check if a tool:command pattern is allowed
904
- */
905
- async isAllowed(tool, command) {
906
- const s = await this.load();
907
- const pattern = `${tool}:${command}`;
908
- for (const deny of s.permissions.deny) {
909
- if (this.matchPattern(pattern, deny)) {
910
- return false;
911
- }
912
- }
913
- for (const allow of s.permissions.allow) {
914
- if (this.matchPattern(pattern, allow)) {
915
- return true;
916
- }
917
- }
918
- return false;
1188
+ await fs4.writeFile(this.configPath, JSON.stringify(config2, null, 2));
1189
+ this.clearCache();
919
1190
  }
920
- /**
921
- * Check if a tool:command pattern is explicitly denied
922
- */
923
- async isDenied(tool, command) {
924
- const s = await this.load();
925
- const pattern = `${tool}:${command}`;
926
- for (const deny of s.permissions.deny) {
927
- if (this.matchPattern(pattern, deny)) {
928
- return true;
929
- }
1191
+ async exists() {
1192
+ try {
1193
+ await fs4.access(this.configPath);
1194
+ return true;
1195
+ } catch {
1196
+ return false;
930
1197
  }
931
- return false;
932
- }
933
- /**
934
- * Simple glob-like pattern matching
935
- * Supports * as wildcard
936
- */
937
- matchPattern(value, pattern) {
938
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
939
- return new RegExp(`^${regex}$`).test(value);
940
- }
941
- clearCache() {
942
- this.cached = null;
943
1198
  }
944
1199
  getPath() {
945
- return this.settingsPath;
1200
+ return this.configPath;
946
1201
  }
947
1202
  };
948
- var settings = new SettingsManager();
1203
+ var config = new ConfigManager();
949
1204
 
950
1205
  // src/core/context.ts
1206
+ import fs5 from "fs/promises";
1207
+ import path5 from "path";
951
1208
  var CONTEXT_DIR = ".obsidian";
952
1209
  var CONTEXT_FILE = "context.json";
953
1210
  function generateSessionId() {
@@ -1079,8 +1336,8 @@ import Anthropic from "@anthropic-ai/sdk";
1079
1336
  // src/core/tools.ts
1080
1337
  import { exec as exec2 } from "child_process";
1081
1338
  import { promisify as promisify2 } from "util";
1082
- import fs8 from "fs/promises";
1083
- import path8 from "path";
1339
+ import fs9 from "fs/promises";
1340
+ import path9 from "path";
1084
1341
 
1085
1342
  // src/core/auditor.ts
1086
1343
  import path6 from "path";
@@ -1095,12 +1352,17 @@ var BLOCKED_PATTERNS = [
1095
1352
  "mkfs",
1096
1353
  "dd if=",
1097
1354
  "chmod -R 777",
1098
- ":(){ :|:& };:",
1099
- "curl | sh",
1100
- // Pipe to shell
1101
- "wget | sh",
1102
- "curl | bash",
1103
- "wget | bash"
1355
+ ":(){ :|:& };:"
1356
+ ];
1357
+ var BLOCKED_REGEX_PATTERNS = [
1358
+ /curl\s+[^\|]+\|\s*(sh|bash)/i,
1359
+ // curl URL | sh/bash
1360
+ /wget\s+[^\|]+\|\s*(sh|bash)/i,
1361
+ // wget URL | sh/bash
1362
+ /curl\s*\|\s*(sh|bash)/i,
1363
+ // curl | sh/bash (direct)
1364
+ /wget\s*\|\s*(sh|bash)/i
1365
+ // wget | sh/bash (direct)
1104
1366
  ];
1105
1367
  var APPROVAL_PATTERNS = [
1106
1368
  { pattern: "rm -rf", reason: "Recursive delete operation" },
@@ -1127,6 +1389,13 @@ var Auditor = class {
1127
1389
  isCritical: true
1128
1390
  };
1129
1391
  }
1392
+ if (BLOCKED_REGEX_PATTERNS.some((p) => p.test(command))) {
1393
+ return {
1394
+ approved: false,
1395
+ reason: "Detected dangerous pipe-to-shell pattern",
1396
+ isCritical: true
1397
+ };
1398
+ }
1130
1399
  if (await settings.isDenied("bash", command)) {
1131
1400
  return {
1132
1401
  approved: false,
@@ -1143,7 +1412,7 @@ var Auditor = class {
1143
1412
  for (const { pattern, reason } of APPROVAL_PATTERNS) {
1144
1413
  if (lowerCommand.includes(pattern.toLowerCase())) {
1145
1414
  return {
1146
- approved: true,
1415
+ approved: false,
1147
1416
  requiresApproval: true,
1148
1417
  reason
1149
1418
  };
@@ -1152,7 +1421,7 @@ var Auditor = class {
1152
1421
  const s = await settings.load();
1153
1422
  if (s.mode === "safe") {
1154
1423
  return {
1155
- approved: true,
1424
+ approved: false,
1156
1425
  requiresApproval: true,
1157
1426
  reason: "Safe mode requires approval for all commands"
1158
1427
  };
@@ -1449,56 +1718,279 @@ var UndoManager = class {
1449
1718
  if (toUndo.length === 0) {
1450
1719
  return { success: false, message: "Nothing to undo" };
1451
1720
  }
1452
- const results = [];
1453
- for (const change of toUndo) {
1454
- try {
1455
- const fullPath = path7.resolve(process.cwd(), change.filePath);
1456
- switch (change.operation) {
1457
- case "create":
1458
- await fs7.unlink(fullPath);
1459
- results.push(`Deleted: ${change.filePath}`);
1460
- break;
1461
- case "edit":
1462
- if (change.beforeContent !== null) {
1463
- await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
1464
- results.push(`Restored: ${change.filePath}`);
1465
- }
1466
- break;
1467
- case "delete":
1468
- if (change.beforeContent !== null) {
1469
- await fs7.mkdir(path7.dirname(fullPath), { recursive: true });
1470
- await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
1471
- results.push(`Restored: ${change.filePath}`);
1472
- }
1473
- break;
1474
- }
1475
- change.undone = true;
1476
- if (this.dbEnabled && this.prisma) {
1477
- await this.prisma.fileChange.update({
1478
- where: { id: change.id },
1479
- data: { undone: true }
1480
- }).catch(() => {
1481
- });
1721
+ const results = [];
1722
+ for (const change of toUndo) {
1723
+ try {
1724
+ const fullPath = path7.resolve(process.cwd(), change.filePath);
1725
+ switch (change.operation) {
1726
+ case "create":
1727
+ await fs7.unlink(fullPath);
1728
+ results.push(`Deleted: ${change.filePath}`);
1729
+ break;
1730
+ case "edit":
1731
+ if (change.beforeContent !== null) {
1732
+ await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
1733
+ results.push(`Restored: ${change.filePath}`);
1734
+ }
1735
+ break;
1736
+ case "delete":
1737
+ if (change.beforeContent !== null) {
1738
+ await fs7.mkdir(path7.dirname(fullPath), { recursive: true });
1739
+ await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
1740
+ results.push(`Restored: ${change.filePath}`);
1741
+ }
1742
+ break;
1743
+ }
1744
+ change.undone = true;
1745
+ if (this.dbEnabled && this.prisma) {
1746
+ await this.prisma.fileChange.update({
1747
+ where: { id: change.id },
1748
+ data: { undone: true }
1749
+ }).catch(() => {
1750
+ });
1751
+ }
1752
+ } catch (error) {
1753
+ results.push(`Failed: ${change.filePath} - ${error.message}`);
1754
+ }
1755
+ }
1756
+ return {
1757
+ success: true,
1758
+ message: results.join("\n")
1759
+ };
1760
+ }
1761
+ getHistory(limit = 10) {
1762
+ return this.changes.filter((c) => !c.undone).slice(0, limit);
1763
+ }
1764
+ async close() {
1765
+ if (this.prisma) {
1766
+ await this.prisma.$disconnect();
1767
+ }
1768
+ }
1769
+ };
1770
+ var undo = new UndoManager();
1771
+
1772
+ // src/core/auditLog.ts
1773
+ import fs8 from "fs/promises";
1774
+ import path8 from "path";
1775
+ var LOG_DIR = ".obsidian";
1776
+ var LOG_FILE = "audit.log";
1777
+ var MAX_LOG_SIZE = 10 * 1024 * 1024;
1778
+ var AuditLogger = class {
1779
+ logPath;
1780
+ sessionId;
1781
+ enabled = true;
1782
+ writeQueue = [];
1783
+ isWriting = false;
1784
+ constructor() {
1785
+ this.logPath = path8.join(process.cwd(), LOG_DIR, LOG_FILE);
1786
+ this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1787
+ }
1788
+ /**
1789
+ * Initialize the audit logger
1790
+ */
1791
+ async init() {
1792
+ const s = await settings.load();
1793
+ this.enabled = s.security?.auditLogging ?? true;
1794
+ if (!this.enabled) return;
1795
+ const dir = path8.dirname(this.logPath);
1796
+ await fs8.mkdir(dir, { recursive: true });
1797
+ await this.rotateIfNeeded();
1798
+ await this.log({
1799
+ eventType: "session_start",
1800
+ success: true,
1801
+ metadata: {
1802
+ cwd: process.cwd(),
1803
+ pid: process.pid,
1804
+ nodeVersion: process.version
1805
+ }
1806
+ });
1807
+ }
1808
+ /**
1809
+ * Set the session ID (called from agent)
1810
+ */
1811
+ setSessionId(sessionId) {
1812
+ this.sessionId = sessionId;
1813
+ }
1814
+ /**
1815
+ * Enable or disable logging
1816
+ */
1817
+ setEnabled(enabled) {
1818
+ this.enabled = enabled;
1819
+ }
1820
+ /**
1821
+ * Log a command execution
1822
+ */
1823
+ async logCommand(command, success, reason) {
1824
+ await this.log({
1825
+ eventType: success ? "command_executed" : "command_blocked",
1826
+ tool: "bash",
1827
+ command,
1828
+ success,
1829
+ reason
1830
+ });
1831
+ }
1832
+ /**
1833
+ * Log a file operation
1834
+ */
1835
+ async logFileOperation(operation, filePath, success, reason) {
1836
+ const eventMap = {
1837
+ read: "file_read",
1838
+ write: "file_write",
1839
+ edit: "file_edit",
1840
+ delete: "file_delete"
1841
+ };
1842
+ await this.log({
1843
+ eventType: eventMap[operation],
1844
+ filePath,
1845
+ success,
1846
+ reason
1847
+ });
1848
+ }
1849
+ /**
1850
+ * Log an approval decision
1851
+ */
1852
+ async logApproval(status, command, reason) {
1853
+ const eventMap = {
1854
+ requested: "approval_requested",
1855
+ granted: "approval_granted",
1856
+ denied: "approval_denied",
1857
+ timeout: "approval_timeout"
1858
+ };
1859
+ await this.log({
1860
+ eventType: eventMap[status],
1861
+ command,
1862
+ success: status === "granted",
1863
+ reason
1864
+ });
1865
+ }
1866
+ /**
1867
+ * Log a security violation
1868
+ */
1869
+ async logSecurityViolation(command, reason) {
1870
+ await this.log({
1871
+ eventType: "security_violation",
1872
+ command,
1873
+ success: false,
1874
+ reason
1875
+ });
1876
+ }
1877
+ /**
1878
+ * Log PII redaction
1879
+ */
1880
+ async logRedaction(count, types) {
1881
+ await this.log({
1882
+ eventType: "pii_redacted",
1883
+ success: true,
1884
+ metadata: { count, types }
1885
+ });
1886
+ }
1887
+ /**
1888
+ * Core logging function
1889
+ */
1890
+ async log(entry) {
1891
+ if (!this.enabled) return;
1892
+ const fullEntry = {
1893
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1894
+ sessionId: this.sessionId,
1895
+ ...entry
1896
+ };
1897
+ this.writeQueue.push(fullEntry);
1898
+ if (!this.isWriting) {
1899
+ await this.processQueue();
1900
+ }
1901
+ }
1902
+ /**
1903
+ * Process the write queue
1904
+ */
1905
+ async processQueue() {
1906
+ if (this.isWriting || this.writeQueue.length === 0) return;
1907
+ this.isWriting = true;
1908
+ try {
1909
+ const entries = [...this.writeQueue];
1910
+ this.writeQueue = [];
1911
+ const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
1912
+ await fs8.appendFile(this.logPath, lines, { encoding: "utf-8", mode: 384 });
1913
+ } catch (error) {
1914
+ } finally {
1915
+ this.isWriting = false;
1916
+ if (this.writeQueue.length > 0) {
1917
+ setImmediate(() => this.processQueue());
1918
+ }
1919
+ }
1920
+ }
1921
+ /**
1922
+ * Rotate log file if too large
1923
+ */
1924
+ async rotateIfNeeded() {
1925
+ try {
1926
+ const stats = await fs8.stat(this.logPath);
1927
+ if (stats.size >= MAX_LOG_SIZE) {
1928
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1929
+ const rotatedPath = this.logPath.replace(".log", `.${timestamp}.log`);
1930
+ await fs8.rename(this.logPath, rotatedPath);
1931
+ await this.cleanupOldLogs();
1932
+ }
1933
+ } catch {
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Clean up old rotated log files
1938
+ */
1939
+ async cleanupOldLogs() {
1940
+ try {
1941
+ const dir = path8.dirname(this.logPath);
1942
+ const files = await fs8.readdir(dir);
1943
+ const auditLogs = files.filter((f) => f.startsWith("audit.") && f.endsWith(".log") && f !== "audit.log").sort().reverse();
1944
+ for (const file of auditLogs.slice(5)) {
1945
+ await fs8.unlink(path8.join(dir, file));
1946
+ }
1947
+ } catch {
1948
+ }
1949
+ }
1950
+ /**
1951
+ * Read recent audit entries
1952
+ */
1953
+ async getRecentEntries(count = 100) {
1954
+ try {
1955
+ const content = await fs8.readFile(this.logPath, "utf-8");
1956
+ const lines = content.trim().split("\n").filter((l) => l);
1957
+ const entries = lines.slice(-count).map((line) => {
1958
+ try {
1959
+ return JSON.parse(line);
1960
+ } catch {
1961
+ return null;
1482
1962
  }
1483
- } catch (error) {
1484
- results.push(`Failed: ${change.filePath} - ${error.message}`);
1485
- }
1963
+ }).filter((e) => e !== null);
1964
+ return entries;
1965
+ } catch {
1966
+ return [];
1486
1967
  }
1487
- return {
1488
- success: true,
1489
- message: results.join("\n")
1490
- };
1491
1968
  }
1492
- getHistory(limit = 10) {
1493
- return this.changes.filter((c) => !c.undone).slice(0, limit);
1969
+ /**
1970
+ * Search audit log by event type or command
1971
+ */
1972
+ async search(query) {
1973
+ const entries = await this.getRecentEntries(1e3);
1974
+ return entries.filter((e) => {
1975
+ if (query.eventType && e.eventType !== query.eventType) return false;
1976
+ if (query.command && (!e.command || !e.command.includes(query.command))) return false;
1977
+ if (query.since && new Date(e.timestamp) < query.since) return false;
1978
+ return true;
1979
+ });
1494
1980
  }
1981
+ /**
1982
+ * Log session end (call on shutdown)
1983
+ */
1495
1984
  async close() {
1496
- if (this.prisma) {
1497
- await this.prisma.$disconnect();
1498
- }
1985
+ if (!this.enabled) return;
1986
+ await this.log({
1987
+ eventType: "session_end",
1988
+ success: true
1989
+ });
1990
+ await this.processQueue();
1499
1991
  }
1500
1992
  };
1501
- var undo = new UndoManager();
1993
+ var auditLog = new AuditLogger();
1502
1994
 
1503
1995
  // src/core/tools.ts
1504
1996
  var execAsync2 = promisify2(exec2);
@@ -1554,22 +2046,26 @@ var BashTool = {
1554
2046
  return { success: false, error: "No command provided" };
1555
2047
  }
1556
2048
  const audit = await auditor.checkCommand(command);
1557
- if (!audit.approved) {
2049
+ if (!audit.approved && audit.isCritical) {
2050
+ await auditLog.logSecurityViolation(command, audit.reason || "Critical security violation");
1558
2051
  return {
1559
2052
  success: false,
1560
2053
  error: `Security violation: ${audit.reason}`
1561
2054
  };
1562
2055
  }
1563
- if (audit.requiresApproval && !audit.autoApproved) {
2056
+ if (!audit.approved && audit.requiresApproval) {
2057
+ await auditLog.logApproval("requested", command, audit.reason);
1564
2058
  const approved = await requestApproval(command, audit.reason || "Potentially dangerous operation");
1565
2059
  if (!approved) {
1566
2060
  await settings.addDeniedPermission("bash", command);
2061
+ await auditLog.logApproval("denied", command);
1567
2062
  return {
1568
2063
  success: false,
1569
2064
  error: "Command rejected by user"
1570
2065
  };
1571
2066
  }
1572
2067
  await settings.addAllowedPermission("bash", command);
2068
+ await auditLog.logApproval("granted", command);
1573
2069
  }
1574
2070
  try {
1575
2071
  const execCommand = await sandbox.wrapCommand(command);
@@ -1581,11 +2077,13 @@ var BashTool = {
1581
2077
  // 1MB buffer (reduced from 10MB)
1582
2078
  });
1583
2079
  const output = stdout || stderr || "Command executed successfully";
2080
+ await auditLog.logCommand(command, true);
1584
2081
  return {
1585
2082
  success: true,
1586
2083
  output: truncateOutput(output)
1587
2084
  };
1588
2085
  } catch (error) {
2086
+ await auditLog.logCommand(command, false, error.message);
1589
2087
  return {
1590
2088
  success: false,
1591
2089
  error: error.message || "Command execution failed"
@@ -1615,8 +2113,8 @@ var ReadTool = {
1615
2113
  };
1616
2114
  }
1617
2115
  try {
1618
- const fullPath = path8.resolve(process.cwd(), filePath);
1619
- const content = await fs8.readFile(fullPath, "utf-8");
2116
+ const fullPath = path9.resolve(process.cwd(), filePath);
2117
+ const content = await fs9.readFile(fullPath, "utf-8");
1620
2118
  const lines = content.split("\n");
1621
2119
  const limitedLines = lines.slice(0, MAX_FILE_READ_LINES);
1622
2120
  const numbered = limitedLines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
@@ -1624,6 +2122,7 @@ var ReadTool = {
1624
2122
 
1625
2123
  ... [TRUNCATED: ${lines.length - MAX_FILE_READ_LINES} more lines. Use offset parameter to read more.]` : "";
1626
2124
  await context.trackRead(filePath);
2125
+ await auditLog.logFileOperation("read", filePath, true);
1627
2126
  return {
1628
2127
  success: true,
1629
2128
  output: truncateOutput(`File: ${filePath} (${lines.length} lines)
@@ -1631,6 +2130,7 @@ ${"=".repeat(60)}
1631
2130
  ${numbered}${truncationNote}`)
1632
2131
  };
1633
2132
  } catch (error) {
2133
+ await auditLog.logFileOperation("read", filePath, false, error.message);
1634
2134
  return {
1635
2135
  success: false,
1636
2136
  error: `Failed to read file: ${error.message}`
@@ -1658,24 +2158,26 @@ var WriteTool = {
1658
2158
  };
1659
2159
  }
1660
2160
  try {
1661
- const fullPath = path8.resolve(process.cwd(), filePath);
2161
+ const fullPath = path9.resolve(process.cwd(), filePath);
1662
2162
  try {
1663
- await fs8.access(fullPath);
2163
+ await fs9.access(fullPath);
1664
2164
  return {
1665
2165
  success: false,
1666
2166
  error: `File already exists: ${filePath}. Use 'edit' tool to modify.`
1667
2167
  };
1668
2168
  } catch {
1669
2169
  }
1670
- await fs8.mkdir(path8.dirname(fullPath), { recursive: true });
1671
- await fs8.writeFile(fullPath, content, "utf-8");
2170
+ await fs9.mkdir(path9.dirname(fullPath), { recursive: true });
2171
+ await fs9.writeFile(fullPath, content, "utf-8");
1672
2172
  await context.trackModified(filePath);
1673
2173
  await undo.recordChange(filePath, "create", null, content);
2174
+ await auditLog.logFileOperation("write", filePath, true);
1674
2175
  return {
1675
2176
  success: true,
1676
2177
  output: `Created file: ${filePath} (${content.length} bytes)`
1677
2178
  };
1678
2179
  } catch (error) {
2180
+ await auditLog.logFileOperation("write", filePath, false, error.message);
1679
2181
  return {
1680
2182
  success: false,
1681
2183
  error: `Failed to write file: ${error.message}`
@@ -1707,29 +2209,33 @@ var EditTool = {
1707
2209
  };
1708
2210
  }
1709
2211
  try {
1710
- const fullPath = path8.resolve(process.cwd(), filePath);
1711
- const original = await fs8.readFile(fullPath, "utf-8");
2212
+ const fullPath = path9.resolve(process.cwd(), filePath);
2213
+ const original = await fs9.readFile(fullPath, "utf-8");
1712
2214
  if (!original.includes(search)) {
1713
2215
  return {
1714
2216
  success: false,
1715
2217
  error: `Search string not found in ${filePath}`
1716
2218
  };
1717
2219
  }
1718
- const modified = original.replace(search, replace);
2220
+ const occurrences = original.split(search).length - 1;
2221
+ const modified = original.replaceAll(search, replace);
1719
2222
  const diffPreview = generateDiffPreview(search, replace);
1720
- await fs8.writeFile(fullPath, modified, "utf-8");
2223
+ await fs9.writeFile(fullPath, modified, "utf-8");
1721
2224
  const originalLines = original.split("\n").length;
1722
2225
  const modifiedLines = modified.split("\n").length;
1723
2226
  const delta = modifiedLines - originalLines;
1724
2227
  await context.trackModified(filePath);
1725
2228
  await undo.recordChange(filePath, "edit", original, modified);
2229
+ await auditLog.logFileOperation("edit", filePath, true);
2230
+ const occurrenceText = occurrences > 1 ? ` (${occurrences} occurrences)` : "";
1726
2231
  return {
1727
2232
  success: true,
1728
- output: `Edited ${filePath}:
2233
+ output: `Edited ${filePath}${occurrenceText}:
1729
2234
  ${diffPreview}
1730
2235
  Lines: ${originalLines} -> ${modifiedLines} (${delta >= 0 ? "+" : ""}${delta})`
1731
2236
  };
1732
2237
  } catch (error) {
2238
+ await auditLog.logFileOperation("edit", filePath, false, error.message);
1733
2239
  return {
1734
2240
  success: false,
1735
2241
  error: `Failed to edit file: ${error.message}`
@@ -1772,8 +2278,8 @@ var ListTool = {
1772
2278
  };
1773
2279
  }
1774
2280
  try {
1775
- const fullPath = path8.resolve(process.cwd(), dirPath);
1776
- const entries = await fs8.readdir(fullPath, { withFileTypes: true });
2281
+ const fullPath = path9.resolve(process.cwd(), dirPath);
2282
+ const entries = await fs9.readdir(fullPath, { withFileTypes: true });
1777
2283
  const filtered = entries.filter(
1778
2284
  (entry) => !IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith(".")
1779
2285
  );
@@ -1817,7 +2323,7 @@ var GrepTool = {
1817
2323
  };
1818
2324
  }
1819
2325
  try {
1820
- const fullPath = path8.resolve(process.cwd(), searchPath);
2326
+ const fullPath = path9.resolve(process.cwd(), searchPath);
1821
2327
  const results = [];
1822
2328
  await searchDirectory(fullPath, pattern, results, maxResults);
1823
2329
  if (results.length === 0) {
@@ -1843,23 +2349,23 @@ ${results.join("\n")}`)
1843
2349
  async function searchDirectory(dir, pattern, results, maxResults, depth = 0) {
1844
2350
  if (results.length >= maxResults || depth > 10) return;
1845
2351
  try {
1846
- const entries = await fs8.readdir(dir, { withFileTypes: true });
2352
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
1847
2353
  const regex = new RegExp(pattern, "gi");
1848
2354
  for (const entry of entries) {
1849
2355
  if (results.length >= maxResults) break;
1850
- const fullPath = path8.join(dir, entry.name);
1851
- const relativePath = path8.relative(process.cwd(), fullPath);
2356
+ const fullPath = path9.join(dir, entry.name);
2357
+ const relativePath = path9.relative(process.cwd(), fullPath);
1852
2358
  if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
1853
2359
  continue;
1854
2360
  }
1855
2361
  if (entry.isDirectory()) {
1856
2362
  await searchDirectory(fullPath, pattern, results, maxResults, depth + 1);
1857
2363
  } else if (entry.isFile()) {
1858
- const ext = path8.extname(entry.name).toLowerCase();
2364
+ const ext = path9.extname(entry.name).toLowerCase();
1859
2365
  const textExtensions = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".yaml", ".yml", ".css", ".html", ".sh"];
1860
2366
  if (textExtensions.includes(ext) || ext === "") {
1861
2367
  try {
1862
- const content = await fs8.readFile(fullPath, "utf-8");
2368
+ const content = await fs9.readFile(fullPath, "utf-8");
1863
2369
  const lines = content.split("\n");
1864
2370
  for (let i = 0; i < lines.length && results.length < maxResults; i++) {
1865
2371
  if (regex.test(lines[i])) {
@@ -1886,7 +2392,7 @@ var GlobTool = {
1886
2392
  }
1887
2393
  try {
1888
2394
  const results = [];
1889
- const fullBase = path8.resolve(process.cwd(), basePath);
2395
+ const fullBase = path9.resolve(process.cwd(), basePath);
1890
2396
  await globSearch(fullBase, pattern, results, 100);
1891
2397
  if (results.length === 0) {
1892
2398
  return {
@@ -1910,7 +2416,7 @@ ${results.join("\n")}`)
1910
2416
  async function globSearch(dir, pattern, results, maxResults, depth = 0) {
1911
2417
  if (results.length >= maxResults || depth > 15) return;
1912
2418
  try {
1913
- const entries = await fs8.readdir(dir, { withFileTypes: true });
2419
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
1914
2420
  const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
1915
2421
  const regex = new RegExp(`^${regexPattern}$`);
1916
2422
  for (const entry of entries) {
@@ -1918,8 +2424,8 @@ async function globSearch(dir, pattern, results, maxResults, depth = 0) {
1918
2424
  if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
1919
2425
  continue;
1920
2426
  }
1921
- const fullPath = path8.join(dir, entry.name);
1922
- const relativePath = path8.relative(process.cwd(), fullPath);
2427
+ const fullPath = path9.join(dir, entry.name);
2428
+ const relativePath = path9.relative(process.cwd(), fullPath);
1923
2429
  if (entry.isDirectory()) {
1924
2430
  if (pattern.includes("**") || pattern.includes("/")) {
1925
2431
  await globSearch(fullPath, pattern, results, maxResults, depth + 1);
@@ -2048,6 +2554,520 @@ var ToolRegistry = class {
2048
2554
  };
2049
2555
  var tools = new ToolRegistry();
2050
2556
 
2557
+ // src/core/redactor.ts
2558
+ var BUILTIN_RULES = [
2559
+ // Email addresses
2560
+ {
2561
+ name: "email",
2562
+ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
2563
+ replacement: "[REDACTED:email]",
2564
+ enabled: true,
2565
+ description: "Email addresses"
2566
+ },
2567
+ // Phone numbers (various formats) - must have separators to avoid false positives
2568
+ {
2569
+ name: "phone",
2570
+ pattern: /(\+?1[-.\s])?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g,
2571
+ replacement: "[REDACTED:phone]",
2572
+ enabled: true,
2573
+ description: "Phone numbers (US format with separators)"
2574
+ },
2575
+ // Social Security Numbers
2576
+ {
2577
+ name: "ssn",
2578
+ pattern: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
2579
+ replacement: "[REDACTED:ssn]",
2580
+ enabled: true,
2581
+ description: "Social Security Numbers"
2582
+ },
2583
+ // Credit card numbers (basic patterns)
2584
+ {
2585
+ name: "credit-card",
2586
+ pattern: /\b(?:4\d{3}|5[1-5]\d{2}|6(?:011|5\d{2})|3[47]\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
2587
+ replacement: "[REDACTED:credit-card]",
2588
+ enabled: true,
2589
+ description: "Credit card numbers"
2590
+ },
2591
+ // AWS Access Keys
2592
+ {
2593
+ name: "aws-access-key",
2594
+ pattern: /\b(AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}\b/g,
2595
+ replacement: "[REDACTED:aws-key]",
2596
+ enabled: true,
2597
+ description: "AWS access key IDs"
2598
+ },
2599
+ // AWS Secret Keys (40 char base64-like)
2600
+ {
2601
+ name: "aws-secret-key",
2602
+ pattern: /\b[A-Za-z0-9/+=]{40}\b/g,
2603
+ replacement: (match) => {
2604
+ if (/[A-Z]/.test(match) && /[a-z]/.test(match) && /[/+=]/.test(match)) {
2605
+ return "[REDACTED:aws-secret]";
2606
+ }
2607
+ return match;
2608
+ },
2609
+ enabled: true,
2610
+ description: "AWS secret access keys"
2611
+ },
2612
+ // Anthropic API Keys
2613
+ {
2614
+ name: "anthropic-key",
2615
+ pattern: /\bsk-ant-[a-zA-Z0-9-_]{20,}/g,
2616
+ replacement: "[REDACTED:anthropic-key]",
2617
+ enabled: true,
2618
+ description: "Anthropic API keys"
2619
+ },
2620
+ // OpenAI API Keys (start with sk- but not sk-ant-)
2621
+ {
2622
+ name: "openai-key",
2623
+ pattern: /sk-(?!ant)[a-zA-Z0-9]{20,}/g,
2624
+ replacement: "[REDACTED:openai-key]",
2625
+ enabled: true,
2626
+ description: "OpenAI API keys"
2627
+ },
2628
+ // Generic API keys (common patterns)
2629
+ {
2630
+ name: "api-key-generic",
2631
+ pattern: /\b(api[_-]?key|apikey|api[_-]?secret|api[_-]?token)\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
2632
+ replacement: (match) => {
2633
+ const keyName = match.split(/[=:]/)[0].trim();
2634
+ return `${keyName}=[REDACTED:api-key]`;
2635
+ },
2636
+ enabled: true,
2637
+ description: "Generic API keys in config"
2638
+ },
2639
+ // Passwords in config files (various naming conventions)
2640
+ {
2641
+ name: "password-config",
2642
+ pattern: /\b([A-Z_]*(?:PASSWORD|PASSWD|PWD|SECRET)[A-Z_]*)\s*[=:]\s*['"]?([^'"\s\n]{4,})['"]?/gi,
2643
+ replacement: (match) => {
2644
+ const keyName = match.split(/[=:]/)[0].trim();
2645
+ return `${keyName}=[REDACTED:password]`;
2646
+ },
2647
+ enabled: true,
2648
+ description: "Passwords in configuration"
2649
+ },
2650
+ // Private keys (PEM format)
2651
+ {
2652
+ name: "private-key",
2653
+ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/g,
2654
+ replacement: "[REDACTED:private-key]",
2655
+ enabled: true,
2656
+ description: "PEM private keys"
2657
+ },
2658
+ // JWT tokens
2659
+ {
2660
+ name: "jwt",
2661
+ pattern: /\beyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
2662
+ replacement: "[REDACTED:jwt]",
2663
+ enabled: true,
2664
+ description: "JWT tokens"
2665
+ },
2666
+ // GitHub tokens (various prefixes)
2667
+ {
2668
+ name: "github-token",
2669
+ pattern: /(ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{20,}/g,
2670
+ replacement: "[REDACTED:github-token]",
2671
+ enabled: true,
2672
+ description: "GitHub tokens"
2673
+ },
2674
+ // Stripe keys
2675
+ {
2676
+ name: "stripe-key",
2677
+ pattern: /\b(sk|pk)_(test|live)_[a-zA-Z0-9]{24,}/g,
2678
+ replacement: "[REDACTED:stripe-key]",
2679
+ enabled: true,
2680
+ description: "Stripe API keys"
2681
+ },
2682
+ // IP addresses (private networks only by default)
2683
+ {
2684
+ name: "private-ip",
2685
+ pattern: /\b(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
2686
+ replacement: "[REDACTED:private-ip]",
2687
+ enabled: false,
2688
+ // Disabled by default - may cause issues with code
2689
+ description: "Private IP addresses"
2690
+ }
2691
+ ];
2692
+ var Redactor = class {
2693
+ rules = [...BUILTIN_RULES];
2694
+ enabled = true;
2695
+ allowlist = /* @__PURE__ */ new Set();
2696
+ /**
2697
+ * Enable or disable redaction globally
2698
+ */
2699
+ setEnabled(enabled) {
2700
+ this.enabled = enabled;
2701
+ }
2702
+ /**
2703
+ * Check if redaction is enabled
2704
+ */
2705
+ isEnabled() {
2706
+ return this.enabled;
2707
+ }
2708
+ /**
2709
+ * Enable or disable a specific rule
2710
+ */
2711
+ setRuleEnabled(ruleName, enabled) {
2712
+ const rule = this.rules.find((r) => r.name === ruleName);
2713
+ if (rule) {
2714
+ rule.enabled = enabled;
2715
+ }
2716
+ }
2717
+ /**
2718
+ * Add a pattern to the allowlist (won't be redacted)
2719
+ */
2720
+ addToAllowlist(pattern) {
2721
+ this.allowlist.add(pattern);
2722
+ }
2723
+ /**
2724
+ * Remove a pattern from the allowlist
2725
+ */
2726
+ removeFromAllowlist(pattern) {
2727
+ this.allowlist.delete(pattern);
2728
+ }
2729
+ /**
2730
+ * Add a custom redaction rule
2731
+ */
2732
+ addRule(rule) {
2733
+ this.rules.push(rule);
2734
+ }
2735
+ /**
2736
+ * Get all available rules
2737
+ */
2738
+ getRules() {
2739
+ return this.rules;
2740
+ }
2741
+ /**
2742
+ * Redact sensitive information from text
2743
+ */
2744
+ redact(text) {
2745
+ if (!this.enabled || !text) {
2746
+ return { text, redactionCount: 0, redactedTypes: [] };
2747
+ }
2748
+ let result = text;
2749
+ let totalRedactions = 0;
2750
+ const redactedTypes = /* @__PURE__ */ new Set();
2751
+ for (const rule of this.rules) {
2752
+ if (!rule.enabled) continue;
2753
+ rule.pattern.lastIndex = 0;
2754
+ const matches = text.match(rule.pattern);
2755
+ if (matches) {
2756
+ for (const match of matches) {
2757
+ if (this.allowlist.has(match)) continue;
2758
+ const replacement = typeof rule.replacement === "function" ? rule.replacement(match) : rule.replacement;
2759
+ if (replacement !== match) {
2760
+ result = result.replace(match, replacement);
2761
+ totalRedactions++;
2762
+ redactedTypes.add(rule.name);
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ return {
2768
+ text: result,
2769
+ redactionCount: totalRedactions,
2770
+ redactedTypes: Array.from(redactedTypes)
2771
+ };
2772
+ }
2773
+ /**
2774
+ * Redact tool output specifically (used before sending to LLM)
2775
+ */
2776
+ redactToolOutput(toolName, output) {
2777
+ return this.redact(output);
2778
+ }
2779
+ /**
2780
+ * Check if text contains any sensitive patterns
2781
+ */
2782
+ containsSensitiveData(text) {
2783
+ if (!text) {
2784
+ return { hasSensitive: false, types: [] };
2785
+ }
2786
+ const types = [];
2787
+ for (const rule of this.rules) {
2788
+ if (!rule.enabled) continue;
2789
+ rule.pattern.lastIndex = 0;
2790
+ if (rule.pattern.test(text)) {
2791
+ types.push(rule.name);
2792
+ }
2793
+ }
2794
+ return { hasSensitive: types.length > 0, types };
2795
+ }
2796
+ /**
2797
+ * Get statistics about what would be redacted
2798
+ */
2799
+ analyze(text) {
2800
+ const stats = [];
2801
+ for (const rule of this.rules) {
2802
+ if (!rule.enabled) continue;
2803
+ rule.pattern.lastIndex = 0;
2804
+ const matches = text.match(rule.pattern) || [];
2805
+ if (matches.length > 0) {
2806
+ stats.push({
2807
+ ruleName: rule.name,
2808
+ count: matches.length,
2809
+ // Only show first 3 examples, partially masked
2810
+ examples: matches.slice(0, 3).map(
2811
+ (m) => m.length > 10 ? m.slice(0, 5) + "..." + m.slice(-3) : "***"
2812
+ )
2813
+ });
2814
+ }
2815
+ }
2816
+ return stats;
2817
+ }
2818
+ };
2819
+ var redactor = new Redactor();
2820
+
2821
+ // src/core/keyManager.ts
2822
+ import { exec as exec3 } from "child_process";
2823
+ import { promisify as promisify3 } from "util";
2824
+ import fs10 from "fs/promises";
2825
+ import path10 from "path";
2826
+ import os5 from "os";
2827
+ import crypto from "crypto";
2828
+ var execAsync3 = promisify3(exec3);
2829
+ var ROTATION_CHECK_INTERVAL = 4 * 60 * 60 * 1e3;
2830
+ var SERVICE_NAME = "obsidian-next";
2831
+ var ACCOUNT_NAME = "anthropic-api-key";
2832
+ var KeyManager = class {
2833
+ currentKey = null;
2834
+ encryptedFilePath;
2835
+ machineId = null;
2836
+ constructor() {
2837
+ this.encryptedFilePath = path10.join(os5.homedir(), ".obsidian", ".keystore");
2838
+ }
2839
+ /**
2840
+ * Load API key from the most secure available backend
2841
+ */
2842
+ async loadKey() {
2843
+ if (this.currentKey && !this.shouldRotate()) {
2844
+ return this.currentKey.key;
2845
+ }
2846
+ let key = null;
2847
+ let backend = "env";
2848
+ key = process.env.ANTHROPIC_API_KEY || null;
2849
+ if (key) {
2850
+ backend = "env";
2851
+ }
2852
+ if (!key && process.platform === "darwin") {
2853
+ key = await this.loadFromKeychain();
2854
+ if (key) backend = "keychain";
2855
+ }
2856
+ if (!key && process.platform === "linux") {
2857
+ key = await this.loadFromSecretTool();
2858
+ if (key) backend = "secret-tool";
2859
+ }
2860
+ if (!key) {
2861
+ key = await this.loadFromEncryptedFile();
2862
+ if (key) backend = "encrypted-file";
2863
+ }
2864
+ if (key) {
2865
+ this.currentKey = {
2866
+ key,
2867
+ loadedAt: Date.now(),
2868
+ backend
2869
+ };
2870
+ }
2871
+ return key;
2872
+ }
2873
+ /**
2874
+ * Store API key in the most secure available backend
2875
+ */
2876
+ async storeKey(key) {
2877
+ if (process.platform === "darwin") {
2878
+ const result2 = await this.storeInKeychain(key);
2879
+ if (result2.success) {
2880
+ this.currentKey = { key, loadedAt: Date.now(), backend: "keychain" };
2881
+ return { success: true, backend: "keychain" };
2882
+ }
2883
+ }
2884
+ if (process.platform === "linux") {
2885
+ const result2 = await this.storeInSecretTool(key);
2886
+ if (result2.success) {
2887
+ this.currentKey = { key, loadedAt: Date.now(), backend: "secret-tool" };
2888
+ return { success: true, backend: "secret-tool" };
2889
+ }
2890
+ }
2891
+ const result = await this.storeInEncryptedFile(key);
2892
+ if (result.success) {
2893
+ this.currentKey = { key, loadedAt: Date.now(), backend: "encrypted-file" };
2894
+ return { success: true, backend: "encrypted-file" };
2895
+ }
2896
+ return { success: false, backend: "env", error: result.error };
2897
+ }
2898
+ /**
2899
+ * Delete stored key from all backends
2900
+ */
2901
+ async deleteKey() {
2902
+ this.currentKey = null;
2903
+ if (process.platform === "darwin") {
2904
+ await this.deleteFromKeychain();
2905
+ }
2906
+ if (process.platform === "linux") {
2907
+ await this.deleteFromSecretTool();
2908
+ }
2909
+ try {
2910
+ await fs10.unlink(this.encryptedFilePath);
2911
+ } catch {
2912
+ }
2913
+ }
2914
+ /**
2915
+ * Check if key should be rotated (for long-running sessions)
2916
+ */
2917
+ shouldRotate() {
2918
+ if (!this.currentKey) return true;
2919
+ return Date.now() - this.currentKey.loadedAt > ROTATION_CHECK_INTERVAL;
2920
+ }
2921
+ /**
2922
+ * Force reload key from backend
2923
+ */
2924
+ async refreshKey() {
2925
+ this.currentKey = null;
2926
+ return this.loadKey();
2927
+ }
2928
+ /**
2929
+ * Get current backend being used
2930
+ */
2931
+ getBackend() {
2932
+ return this.currentKey?.backend || null;
2933
+ }
2934
+ // ==================== macOS Keychain ====================
2935
+ async loadFromKeychain() {
2936
+ try {
2937
+ const { stdout } = await execAsync3(
2938
+ `security find-generic-password -s "${SERVICE_NAME}" -a "${ACCOUNT_NAME}" -w 2>/dev/null`
2939
+ );
2940
+ return stdout.trim() || null;
2941
+ } catch {
2942
+ return null;
2943
+ }
2944
+ }
2945
+ async storeInKeychain(key) {
2946
+ try {
2947
+ await this.deleteFromKeychain();
2948
+ await execAsync3(
2949
+ `security add-generic-password -s "${SERVICE_NAME}" -a "${ACCOUNT_NAME}" -w "${key}" -U`
2950
+ );
2951
+ return { success: true };
2952
+ } catch (error) {
2953
+ return { success: false, error: error.message };
2954
+ }
2955
+ }
2956
+ async deleteFromKeychain() {
2957
+ try {
2958
+ await execAsync3(
2959
+ `security delete-generic-password -s "${SERVICE_NAME}" -a "${ACCOUNT_NAME}" 2>/dev/null`
2960
+ );
2961
+ } catch {
2962
+ }
2963
+ }
2964
+ // ==================== Linux secret-tool ====================
2965
+ async loadFromSecretTool() {
2966
+ try {
2967
+ const { stdout } = await execAsync3(
2968
+ `secret-tool lookup service "${SERVICE_NAME}" account "${ACCOUNT_NAME}" 2>/dev/null`
2969
+ );
2970
+ return stdout.trim() || null;
2971
+ } catch {
2972
+ return null;
2973
+ }
2974
+ }
2975
+ async storeInSecretTool(key) {
2976
+ try {
2977
+ await execAsync3(
2978
+ `echo -n "${key}" | secret-tool store --label="Obsidian Next API Key" service "${SERVICE_NAME}" account "${ACCOUNT_NAME}"`
2979
+ );
2980
+ return { success: true };
2981
+ } catch (error) {
2982
+ return { success: false, error: error.message };
2983
+ }
2984
+ }
2985
+ async deleteFromSecretTool() {
2986
+ try {
2987
+ await execAsync3(
2988
+ `secret-tool clear service "${SERVICE_NAME}" account "${ACCOUNT_NAME}" 2>/dev/null`
2989
+ );
2990
+ } catch {
2991
+ }
2992
+ }
2993
+ // ==================== Encrypted File ====================
2994
+ async getMachineId() {
2995
+ if (this.machineId) return this.machineId;
2996
+ const components = [
2997
+ os5.hostname(),
2998
+ os5.userInfo().username,
2999
+ os5.platform(),
3000
+ os5.arch()
3001
+ ];
3002
+ if (process.platform === "darwin") {
3003
+ try {
3004
+ const { stdout } = await execAsync3("ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID");
3005
+ const match = stdout.match(/"IOPlatformUUID" = "([^"]+)"/);
3006
+ if (match) components.push(match[1]);
3007
+ } catch {
3008
+ }
3009
+ } else if (process.platform === "linux") {
3010
+ try {
3011
+ const machineId = await fs10.readFile("/etc/machine-id", "utf-8");
3012
+ components.push(machineId.trim());
3013
+ } catch {
3014
+ }
3015
+ }
3016
+ this.machineId = crypto.createHash("sha256").update(components.join(":")).digest("hex");
3017
+ return this.machineId;
3018
+ }
3019
+ async deriveEncryptionKey() {
3020
+ const machineId = await this.getMachineId();
3021
+ return crypto.pbkdf2Sync(machineId, "obsidian-next-salt", 1e5, 32, "sha256");
3022
+ }
3023
+ async loadFromEncryptedFile() {
3024
+ try {
3025
+ const encrypted = await fs10.readFile(this.encryptedFilePath, "utf-8");
3026
+ const data = JSON.parse(encrypted);
3027
+ const key = await this.deriveEncryptionKey();
3028
+ const iv = Buffer.from(data.iv, "hex");
3029
+ const authTag = Buffer.from(data.tag, "hex");
3030
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
3031
+ decipher.setAuthTag(authTag);
3032
+ let decrypted = decipher.update(data.encrypted, "hex", "utf-8");
3033
+ decrypted += decipher.final("utf-8");
3034
+ return decrypted;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ }
3039
+ async storeInEncryptedFile(apiKey) {
3040
+ try {
3041
+ const key = await this.deriveEncryptionKey();
3042
+ const iv = crypto.randomBytes(16);
3043
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
3044
+ let encrypted = cipher.update(apiKey, "utf-8", "hex");
3045
+ encrypted += cipher.final("hex");
3046
+ const authTag = cipher.getAuthTag();
3047
+ const data = {
3048
+ encrypted,
3049
+ iv: iv.toString("hex"),
3050
+ tag: authTag.toString("hex"),
3051
+ version: 1
3052
+ };
3053
+ await fs10.mkdir(path10.dirname(this.encryptedFilePath), { recursive: true });
3054
+ await fs10.writeFile(this.encryptedFilePath, JSON.stringify(data), { mode: 384 });
3055
+ return { success: true };
3056
+ } catch (error) {
3057
+ return { success: false, error: error.message };
3058
+ }
3059
+ }
3060
+ /**
3061
+ * Clear key from memory (call when done with sensitive operations)
3062
+ */
3063
+ clearFromMemory() {
3064
+ if (this.currentKey) {
3065
+ this.currentKey = null;
3066
+ }
3067
+ }
3068
+ };
3069
+ var keyManager = new KeyManager();
3070
+
2051
3071
  // src/core/llm.ts
2052
3072
  var MAX_TOOL_ITERATIONS = 10;
2053
3073
  var LLMClient = class {
@@ -2060,19 +3080,44 @@ var LLMClient = class {
2060
3080
  async initialize() {
2061
3081
  const cfg = await config.load();
2062
3082
  await usage.init();
2063
- if (!cfg.apiKey) {
3083
+ let apiKey = await keyManager.loadKey();
3084
+ if (!apiKey) {
3085
+ apiKey = cfg.apiKey || null;
3086
+ }
3087
+ if (!apiKey) {
2064
3088
  bus.emitAgent({
2065
3089
  type: "error",
2066
- message: "Missing ANTHROPIC_API_KEY. Please set it in .env or via /init."
3090
+ message: "Missing ANTHROPIC_API_KEY. Set via environment, keychain, or /init command."
2067
3091
  });
2068
3092
  return false;
2069
3093
  }
2070
3094
  this.client = new Anthropic({
2071
- apiKey: cfg.apiKey
3095
+ apiKey
2072
3096
  });
2073
3097
  this.lastConfig = cfg;
3098
+ const backend = keyManager.getBackend();
3099
+ if (backend && backend !== "env") {
3100
+ bus.emitAgent({
3101
+ type: "thought",
3102
+ content: `API key loaded from: ${backend}`,
3103
+ hidden: true
3104
+ });
3105
+ }
2074
3106
  return true;
2075
3107
  }
3108
+ /**
3109
+ * Refresh the API client if key has rotated
3110
+ */
3111
+ async refreshIfNeeded() {
3112
+ if (keyManager.shouldRotate()) {
3113
+ const newKey = await keyManager.refreshKey();
3114
+ if (newKey) {
3115
+ this.client = new Anthropic({ apiKey: newKey });
3116
+ return true;
3117
+ }
3118
+ }
3119
+ return false;
3120
+ }
2076
3121
  async streamChat(userMessage) {
2077
3122
  if (!this.client) {
2078
3123
  const initialized = await this.initialize();
@@ -2220,10 +3265,20 @@ cwd: ${process.cwd()}`;
2220
3265
  const toolResults = [];
2221
3266
  for (const toolUse of toolUses) {
2222
3267
  const result = await tools.execute(toolUse.name, toolUse.input);
3268
+ let outputContent = result.success ? result.output || "Success" : result.error || "Failed";
3269
+ const redactionResult = redactor.redactToolOutput(toolUse.name, outputContent);
3270
+ if (redactionResult.redactionCount > 0) {
3271
+ outputContent = redactionResult.text;
3272
+ bus.emitAgent({
3273
+ type: "thought",
3274
+ content: `[Security] Redacted ${redactionResult.redactionCount} sensitive item(s): ${redactionResult.redactedTypes.join(", ")}`,
3275
+ hidden: true
3276
+ });
3277
+ }
2223
3278
  toolResults.push({
2224
3279
  type: "tool_result",
2225
3280
  tool_use_id: toolUse.id,
2226
- content: result.success ? result.output || "Success" : result.error || "Failed",
3281
+ content: outputContent,
2227
3282
  is_error: !result.success
2228
3283
  });
2229
3284
  }
@@ -2358,24 +3413,24 @@ cwd: ${process.cwd()}`;
2358
3413
  var llm = new LLMClient();
2359
3414
 
2360
3415
  // src/core/tasks.ts
2361
- import fs9 from "fs/promises";
2362
- import path9 from "path";
3416
+ import fs11 from "fs/promises";
3417
+ import path11 from "path";
2363
3418
  var TASKS_DIR = ".obsidian";
2364
3419
  var TASKS_FILE = "tasks.md";
2365
3420
  var TaskTracker = class {
2366
3421
  task = null;
2367
3422
  tasksPath;
2368
3423
  constructor() {
2369
- this.tasksPath = path9.join(process.cwd(), TASKS_DIR, TASKS_FILE);
3424
+ this.tasksPath = path11.join(process.cwd(), TASKS_DIR, TASKS_FILE);
2370
3425
  }
2371
3426
  async init() {
2372
- const dir = path9.join(process.cwd(), TASKS_DIR);
2373
- await fs9.mkdir(dir, { recursive: true });
3427
+ const dir = path11.join(process.cwd(), TASKS_DIR);
3428
+ await fs11.mkdir(dir, { recursive: true });
2374
3429
  await this.load();
2375
3430
  }
2376
3431
  async load() {
2377
3432
  try {
2378
- const content = await fs9.readFile(this.tasksPath, "utf-8");
3433
+ const content = await fs11.readFile(this.tasksPath, "utf-8");
2379
3434
  this.task = this.parse(content);
2380
3435
  } catch {
2381
3436
  this.task = null;
@@ -2445,7 +3500,7 @@ var TaskTracker = class {
2445
3500
  }
2446
3501
  async save() {
2447
3502
  const content = this.serialize();
2448
- await fs9.writeFile(this.tasksPath, content);
3503
+ await fs11.writeFile(this.tasksPath, content);
2449
3504
  }
2450
3505
  // Task management
2451
3506
  async create(title) {
@@ -2519,10 +3574,17 @@ var tasks = new TaskTracker();
2519
3574
  var Agent = class {
2520
3575
  initialized = false;
2521
3576
  pendingPlan = null;
3577
+ sessionId;
3578
+ constructor() {
3579
+ this.sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3580
+ }
2522
3581
  async init() {
2523
3582
  if (this.initialized) return;
2524
3583
  await context.init();
2525
3584
  await tasks.init();
3585
+ await undo.init(this.sessionId);
3586
+ auditLog.setSessionId(this.sessionId);
3587
+ await auditLog.init();
2526
3588
  this.initialized = true;
2527
3589
  }
2528
3590
  async run(input) {
@@ -2596,6 +3658,15 @@ ${this.formatPlan(plan)}`
2596
3658
  [Context: ${ctxSummary}]
2597
3659
  [${taskProgress}]`;
2598
3660
  }
3661
+ const redactionResult = redactor.redact(enhancedInput);
3662
+ if (redactionResult.redactionCount > 0) {
3663
+ enhancedInput = redactionResult.text;
3664
+ bus.emitAgent({
3665
+ type: "thought",
3666
+ content: `[Security] Redacted ${redactionResult.redactionCount} sensitive item(s) from context`,
3667
+ hidden: true
3668
+ });
3669
+ }
2599
3670
  const response = await llm.streamChat(enhancedInput);
2600
3671
  if (response) {
2601
3672
  await context.setLastAction(input.slice(0, 50));
@@ -2698,18 +3769,19 @@ Execute each step carefully. Use available tools as needed.`;
2698
3769
  var agent = new Agent();
2699
3770
 
2700
3771
  // src/ui/Root.tsx
2701
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3772
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2702
3773
  var MAX_EVENTS = 50;
2703
3774
  var Root = () => {
2704
- const [events, setEvents] = useState4([]);
2705
- const [input, setInput] = useState4("");
2706
- const [pendingPrompt, setPendingPrompt] = useState4(null);
3775
+ const [events, setEvents] = useState5([]);
3776
+ const [input, setInput] = useState5("");
3777
+ const [pendingPrompt, setPendingPrompt] = useState5(null);
2707
3778
  const { exit } = useApp();
2708
- const [stats, setStats] = useState4({ cost: 0, model: "Loading...", mode: "safe" });
2709
- const handlePromptResolve = useCallback3(() => {
3779
+ const [stats, setStats] = useState5({ cost: 0, model: "Loading...", mode: "safe" });
3780
+ const [showSettings, setShowSettings] = useState5(false);
3781
+ const handlePromptResolve = useCallback4(() => {
2710
3782
  setPendingPrompt(null);
2711
3783
  }, []);
2712
- useEffect(() => {
3784
+ useEffect2(() => {
2713
3785
  const updateStats = async () => {
2714
3786
  const cfg = await config.load();
2715
3787
  setStats({
@@ -2729,19 +3801,19 @@ var Root = () => {
2729
3801
  bus.off("agent", statHandler);
2730
3802
  };
2731
3803
  }, []);
2732
- useEffect(() => {
3804
+ useEffect2(() => {
2733
3805
  history.load().then((loadedEvents) => {
2734
3806
  if (loadedEvents.length > 0) {
2735
3807
  setEvents(loadedEvents);
2736
3808
  }
2737
3809
  });
2738
3810
  }, []);
2739
- useEffect(() => {
3811
+ useEffect2(() => {
2740
3812
  if (events.length > 0) {
2741
3813
  history.save(events);
2742
3814
  }
2743
3815
  }, [events]);
2744
- useEffect(() => {
3816
+ useEffect2(() => {
2745
3817
  const handler = (event) => {
2746
3818
  if (event.type === "clear_history") {
2747
3819
  setEvents([]);
@@ -2789,22 +3861,22 @@ var Root = () => {
2789
3861
  bus.off("user", userHandler);
2790
3862
  };
2791
3863
  }, []);
2792
- const [inputKey, setInputKey] = useState4(0);
2793
- const [selectedIndex, setSelectedIndex] = useState4(0);
3864
+ const [inputKey, setInputKey] = useState5(0);
3865
+ const [selectedIndex, setSelectedIndex] = useState5(0);
2794
3866
  const query = input.toLowerCase();
2795
3867
  const isCommand = input.startsWith("/");
2796
3868
  const matches = isCommand ? COMMANDS.filter((c) => c.name.startsWith(query)) : [];
2797
- useEffect(() => {
3869
+ useEffect2(() => {
2798
3870
  setSelectedIndex(0);
2799
3871
  }, [input]);
2800
- const cycleMode = useCallback3(async () => {
3872
+ const cycleMode = useCallback4(async () => {
2801
3873
  const modes = ["auto", "plan", "safe"];
2802
3874
  const currentIndex = modes.indexOf(stats.mode);
2803
3875
  const nextMode = modes[(currentIndex + 1) % modes.length];
2804
3876
  await agent.setMode(nextMode);
2805
3877
  setStats((prev) => ({ ...prev, mode: nextMode }));
2806
3878
  }, [stats.mode]);
2807
- useInput3((input2, key) => {
3879
+ useInput4((input2, key) => {
2808
3880
  if (key.shift && key.tab) {
2809
3881
  cycleMode();
2810
3882
  return;
@@ -2836,21 +3908,26 @@ var Root = () => {
2836
3908
  exit();
2837
3909
  return;
2838
3910
  }
3911
+ if (value.trim() === "/settings") {
3912
+ setShowSettings(true);
3913
+ setInput("");
3914
+ return;
3915
+ }
2839
3916
  bus.emitUser({ type: "user_input", content: value });
2840
3917
  setInput("");
2841
3918
  };
2842
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", height: "100%", children: [
2843
- /* @__PURE__ */ jsx7(Dashboard, {}),
2844
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, marginY: 1, overflowY: "hidden", justifyContent: "flex-end", children: events.slice(-MAX_EVENTS).map((event, i) => {
3919
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", height: "100%", children: [
3920
+ /* @__PURE__ */ jsx8(Dashboard, {}),
3921
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", flexGrow: 1, marginY: 1, overflowY: "hidden", justifyContent: "flex-end", children: events.slice(-MAX_EVENTS).map((event, i) => {
2845
3922
  let content = null;
2846
3923
  if (event.type === "user_input") {
2847
- content = /* @__PURE__ */ jsx7(Box7, { flexDirection: "row", paddingX: 1, marginBottom: 0, children: /* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#222222", dimColor: true, children: [
2848
- /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " > " }),
2849
- /* @__PURE__ */ jsx7(Text7, { color: "white", children: event.content }),
2850
- /* @__PURE__ */ jsx7(Text7, { children: " " })
3924
+ content = /* @__PURE__ */ jsx8(Box8, { flexDirection: "row", paddingX: 1, marginBottom: 0, children: /* @__PURE__ */ jsxs8(Text8, { backgroundColor: "#222222", dimColor: true, children: [
3925
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: " > " }),
3926
+ /* @__PURE__ */ jsx8(Text8, { color: "white", children: event.content }),
3927
+ /* @__PURE__ */ jsx8(Text8, { children: " " })
2851
3928
  ] }) }, i);
2852
3929
  } else if (event.type === "thought") {
2853
- content = /* @__PURE__ */ jsx7(AgentLine, { content: event.content }, i);
3930
+ content = /* @__PURE__ */ jsx8(AgentLine, { content: event.content }, i);
2854
3931
  } else if (event.type === "tool_start") {
2855
3932
  let argsSummary = "";
2856
3933
  try {
@@ -2861,37 +3938,37 @@ var Root = () => {
2861
3938
  }
2862
3939
  } catch {
2863
3940
  }
2864
- content = /* @__PURE__ */ jsxs7(Box7, { children: [
2865
- /* @__PURE__ */ jsx7(Text7, { backgroundColor: "#1a1a2e", color: "cyan", children: " \u23FA " }),
2866
- /* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#1a1a2e", color: "white", bold: true, children: [
3941
+ content = /* @__PURE__ */ jsxs8(Box8, { children: [
3942
+ /* @__PURE__ */ jsx8(Text8, { backgroundColor: "#1a1a2e", color: "cyan", children: " \u23FA " }),
3943
+ /* @__PURE__ */ jsxs8(Text8, { backgroundColor: "#1a1a2e", color: "white", bold: true, children: [
2867
3944
  " ",
2868
3945
  event.tool
2869
3946
  ] }),
2870
- /* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#1a1a2e", color: "gray", children: [
3947
+ /* @__PURE__ */ jsxs8(Text8, { backgroundColor: "#1a1a2e", color: "gray", children: [
2871
3948
  "(",
2872
3949
  argsSummary,
2873
3950
  ") "
2874
3951
  ] })
2875
3952
  ] }, i);
2876
3953
  } else if (event.type === "tool_result") {
2877
- content = /* @__PURE__ */ jsx7(ToolOutput, { tool: event.tool, output: event.output, isError: event.isError }, i);
3954
+ content = /* @__PURE__ */ jsx8(ToolOutput, { tool: event.tool, output: event.output, isError: event.isError }, i);
2878
3955
  } else if (event.type === "done") {
2879
- content = /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
3956
+ content = /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
2880
3957
  "[OK] ",
2881
3958
  event.summary
2882
3959
  ] }, i);
2883
3960
  } else if (event.type === "error") {
2884
- content = /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
3961
+ content = /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
2885
3962
  "[ERR] ",
2886
3963
  event.message
2887
3964
  ] }, i);
2888
3965
  } else if (event.type === "clear_history") {
2889
- content = /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[SYS] History cleared" }, i);
3966
+ content = /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "[SYS] History cleared" }, i);
2890
3967
  }
2891
3968
  if (!content) return null;
2892
- return /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: content }, i);
3969
+ return /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: content }, i);
2893
3970
  }) }),
2894
- pendingPrompt?.type === "approval" && /* @__PURE__ */ jsx7(
3971
+ pendingPrompt?.type === "approval" && /* @__PURE__ */ jsx8(
2895
3972
  ApprovalPrompt,
2896
3973
  {
2897
3974
  requestId: pendingPrompt.requestId,
@@ -2900,7 +3977,7 @@ var Root = () => {
2900
3977
  onResolve: handlePromptResolve
2901
3978
  }
2902
3979
  ),
2903
- pendingPrompt?.type === "choice" && /* @__PURE__ */ jsx7(
3980
+ pendingPrompt?.type === "choice" && /* @__PURE__ */ jsx8(
2904
3981
  ChoicePrompt,
2905
3982
  {
2906
3983
  question: pendingPrompt.question,
@@ -2908,17 +3985,18 @@ var Root = () => {
2908
3985
  onResolve: handlePromptResolve
2909
3986
  }
2910
3987
  ),
2911
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2912
- /* @__PURE__ */ jsx7(
3988
+ showSettings && /* @__PURE__ */ jsx8(SettingsMenu, { onClose: () => setShowSettings(false) }),
3989
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3990
+ /* @__PURE__ */ jsx8(
2913
3991
  CommandPopup,
2914
3992
  {
2915
3993
  matches,
2916
3994
  selectedIndex
2917
3995
  }
2918
3996
  ),
2919
- /* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: pendingPrompt ? "gray" : "gray", paddingX: 1, children: [
2920
- /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "> " }),
2921
- /* @__PURE__ */ jsx7(
3997
+ /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: pendingPrompt ? "gray" : "gray", paddingX: 1, children: [
3998
+ /* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "> " }),
3999
+ /* @__PURE__ */ jsx8(
2922
4000
  TextInput,
2923
4001
  {
2924
4002
  value: input,
@@ -2932,8 +4010,8 @@ var Root = () => {
2932
4010
  )
2933
4011
  ] })
2934
4012
  ] }),
2935
- /* @__PURE__ */ jsxs7(
2936
- Box7,
4013
+ /* @__PURE__ */ jsxs8(
4014
+ Box8,
2937
4015
  {
2938
4016
  borderStyle: "single",
2939
4017
  borderTop: false,
@@ -2945,20 +4023,20 @@ var Root = () => {
2945
4023
  flexDirection: "row",
2946
4024
  justifyContent: "space-between",
2947
4025
  children: [
2948
- /* @__PURE__ */ jsx7(Box7, { minWidth: 22, children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
4026
+ /* @__PURE__ */ jsx8(Box8, { minWidth: 22, children: /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
2949
4027
  "[ ",
2950
- /* @__PURE__ */ jsx7(Text7, { color: stats.mode === "plan" ? "yellow" : stats.mode === "auto" ? "green" : "white", children: stats.mode === "auto" ? "auto-accept ON" : stats.mode === "plan" ? "plan mode" : "default" }),
4028
+ /* @__PURE__ */ jsx8(Text8, { color: stats.mode === "plan" ? "yellow" : stats.mode === "auto" ? "green" : "white", children: stats.mode === "auto" ? "auto-accept ON" : stats.mode === "plan" ? "plan mode" : "default" }),
2951
4029
  " ]"
2952
4030
  ] }) }),
2953
- /* @__PURE__ */ jsx7(Box7, { minWidth: 20, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[ Context: 0 files ]" }) }),
2954
- /* @__PURE__ */ jsx7(Box7, { minWidth: 25, justifyContent: "center", children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
4031
+ /* @__PURE__ */ jsx8(Box8, { minWidth: 20, children: /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "[ Context: 0 files ]" }) }),
4032
+ /* @__PURE__ */ jsx8(Box8, { minWidth: 25, justifyContent: "center", children: /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
2955
4033
  "[ Model: ",
2956
- /* @__PURE__ */ jsx7(Text7, { color: "white", children: stats.model }),
4034
+ /* @__PURE__ */ jsx8(Text8, { color: "white", children: stats.model }),
2957
4035
  " ]"
2958
4036
  ] }) }),
2959
- /* @__PURE__ */ jsx7(Box7, { minWidth: 15, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
4037
+ /* @__PURE__ */ jsx8(Box8, { minWidth: 15, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
2960
4038
  "[ Cost: ",
2961
- /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
4039
+ /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
2962
4040
  "$",
2963
4041
  stats.cost.toFixed(4)
2964
4042
  ] }),
@@ -3138,15 +4216,15 @@ Usage: /tool <name> <json-args>`
3138
4216
  };
3139
4217
 
3140
4218
  // src/commands/status.ts
3141
- import os5 from "os";
3142
- import path10 from "path";
4219
+ import os6 from "os";
4220
+ import path12 from "path";
3143
4221
  var statusCommand = async (_args) => {
3144
4222
  const cfg = await config.load();
3145
4223
  const stats = usage.getStats();
3146
- const platform = `${os5.type()} ${os5.release()}`;
4224
+ const platform = `${os6.type()} ${os6.release()}`;
3147
4225
  const nodeVersion = process.version;
3148
4226
  const workspace = process.cwd();
3149
- const workspaceName = path10.basename(workspace);
4227
+ const workspaceName = path12.basename(workspace);
3150
4228
  const sessionCost = usage.getSessionCost();
3151
4229
  const toolList = tools.list().map((t) => t.name).join(", ");
3152
4230
  const statusLines = [
@@ -3713,7 +4791,7 @@ var supervisor = new Supervisor();
3713
4791
  async function main() {
3714
4792
  process.stdout.write("\x1B[?1049h");
3715
4793
  process.stdout.write("\x1Bc");
3716
- const { waitUntilExit, cleanup } = render(React7.createElement(Root), {
4794
+ const { waitUntilExit, cleanup } = render(React8.createElement(Root), {
3717
4795
  patchConsole: false,
3718
4796
  exitOnCtrlC: true
3719
4797
  });