@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.
- package/README.md +86 -12
- package/dist/index.js +1497 -419
- 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
|
|
4
|
+
import React8 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// src/ui/Root.tsx
|
|
8
|
-
import { useState as
|
|
9
|
-
import { Box as
|
|
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/
|
|
427
|
-
import
|
|
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] =
|
|
463
|
-
const [owlState, setOwlState] =
|
|
464
|
-
const [columns, setColumns] =
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
501
|
-
|
|
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__ */
|
|
510
|
-
/* @__PURE__ */
|
|
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__ */
|
|
516
|
-
/* @__PURE__ */
|
|
517
|
-
/* @__PURE__ */
|
|
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__ */
|
|
942
|
+
/* @__PURE__ */ jsx6(Text6, { color: "yellow", children: flareAnim2[flareFrame] }),
|
|
521
943
|
" Obsidian Next"
|
|
522
944
|
] }),
|
|
523
|
-
/* @__PURE__ */
|
|
945
|
+
/* @__PURE__ */ jsx6(Text6, { color: "gray", children: workspace })
|
|
524
946
|
] })
|
|
525
947
|
] }),
|
|
526
|
-
showRightColumn && /* @__PURE__ */
|
|
527
|
-
showRightColumn && /* @__PURE__ */
|
|
528
|
-
/* @__PURE__ */
|
|
529
|
-
/* @__PURE__ */
|
|
530
|
-
/* @__PURE__ */
|
|
531
|
-
/* @__PURE__ */
|
|
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__ */
|
|
535
|
-
/* @__PURE__ */
|
|
956
|
+
/* @__PURE__ */ jsxs6(Text6, { color: "white", children: [
|
|
957
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "/tool" }),
|
|
536
958
|
" Execute tools"
|
|
537
959
|
] }),
|
|
538
|
-
/* @__PURE__ */
|
|
539
|
-
/* @__PURE__ */
|
|
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__ */
|
|
544
|
-
/* @__PURE__ */
|
|
545
|
-
/* @__PURE__ */
|
|
546
|
-
/* @__PURE__ */
|
|
547
|
-
/* @__PURE__ */
|
|
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
|
|
557
|
-
import { jsx as
|
|
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__ */
|
|
579
|
-
|
|
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__ */
|
|
590
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
603
|
-
import
|
|
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 ||
|
|
1031
|
+
this.historyPath = customPath || path2.join(os.homedir(), ".obsidian", "history.json");
|
|
610
1032
|
}
|
|
611
1033
|
async load() {
|
|
612
1034
|
try {
|
|
613
|
-
const data = await
|
|
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 =
|
|
625
|
-
await
|
|
626
|
-
await
|
|
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
|
|
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
|
|
644
|
-
import
|
|
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 =
|
|
648
|
-
totalSessions:
|
|
649
|
-
totalRequests:
|
|
650
|
-
totalInputTokens:
|
|
651
|
-
totalOutputTokens:
|
|
652
|
-
totalCost:
|
|
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 ||
|
|
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
|
|
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 =
|
|
708
|
-
await
|
|
709
|
-
await
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
var
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
constructor() {
|
|
838
|
-
this.
|
|
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.
|
|
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.
|
|
1169
|
+
const data = await fs4.readFile(this.configPath, "utf-8");
|
|
847
1170
|
const parsed = JSON.parse(data);
|
|
848
|
-
|
|
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
|
-
|
|
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(
|
|
878
|
-
const
|
|
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.
|
|
893
|
-
this.
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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.
|
|
1200
|
+
return this.configPath;
|
|
946
1201
|
}
|
|
947
1202
|
};
|
|
948
|
-
var
|
|
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
|
|
1083
|
-
import
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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:
|
|
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:
|
|
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
|
-
}
|
|
1484
|
-
|
|
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
|
-
|
|
1493
|
-
|
|
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.
|
|
1497
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
1619
|
-
const content = await
|
|
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 =
|
|
2161
|
+
const fullPath = path9.resolve(process.cwd(), filePath);
|
|
1662
2162
|
try {
|
|
1663
|
-
await
|
|
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
|
|
1671
|
-
await
|
|
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 =
|
|
1711
|
-
const original = await
|
|
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
|
|
2220
|
+
const occurrences = original.split(search).length - 1;
|
|
2221
|
+
const modified = original.replaceAll(search, replace);
|
|
1719
2222
|
const diffPreview = generateDiffPreview(search, replace);
|
|
1720
|
-
await
|
|
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 =
|
|
1776
|
-
const entries = await
|
|
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 =
|
|
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
|
|
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 =
|
|
1851
|
-
const relativePath =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
1922
|
-
const relativePath =
|
|
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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
2362
|
-
import
|
|
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 =
|
|
3424
|
+
this.tasksPath = path11.join(process.cwd(), TASKS_DIR, TASKS_FILE);
|
|
2370
3425
|
}
|
|
2371
3426
|
async init() {
|
|
2372
|
-
const dir =
|
|
2373
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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] =
|
|
2705
|
-
const [input, setInput] =
|
|
2706
|
-
const [pendingPrompt, setPendingPrompt] =
|
|
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] =
|
|
2709
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3811
|
+
useEffect2(() => {
|
|
2740
3812
|
if (events.length > 0) {
|
|
2741
3813
|
history.save(events);
|
|
2742
3814
|
}
|
|
2743
3815
|
}, [events]);
|
|
2744
|
-
|
|
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] =
|
|
2793
|
-
const [selectedIndex, setSelectedIndex] =
|
|
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
|
-
|
|
3869
|
+
useEffect2(() => {
|
|
2798
3870
|
setSelectedIndex(0);
|
|
2799
3871
|
}, [input]);
|
|
2800
|
-
const cycleMode =
|
|
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
|
-
|
|
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__ */
|
|
2843
|
-
/* @__PURE__ */
|
|
2844
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2848
|
-
/* @__PURE__ */
|
|
2849
|
-
/* @__PURE__ */
|
|
2850
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
2865
|
-
/* @__PURE__ */
|
|
2866
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
3966
|
+
content = /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "[SYS] History cleared" }, i);
|
|
2890
3967
|
}
|
|
2891
3968
|
if (!content) return null;
|
|
2892
|
-
return /* @__PURE__ */
|
|
3969
|
+
return /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: content }, i);
|
|
2893
3970
|
}) }),
|
|
2894
|
-
pendingPrompt?.type === "approval" && /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
2912
|
-
|
|
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__ */
|
|
2920
|
-
/* @__PURE__ */
|
|
2921
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2936
|
-
|
|
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__ */
|
|
4026
|
+
/* @__PURE__ */ jsx8(Box8, { minWidth: 22, children: /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
|
|
2949
4027
|
"[ ",
|
|
2950
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2954
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4034
|
+
/* @__PURE__ */ jsx8(Text8, { color: "white", children: stats.model }),
|
|
2957
4035
|
" ]"
|
|
2958
4036
|
] }) }),
|
|
2959
|
-
/* @__PURE__ */
|
|
4037
|
+
/* @__PURE__ */ jsx8(Box8, { minWidth: 15, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs8(Text8, { color: "gray", children: [
|
|
2960
4038
|
"[ Cost: ",
|
|
2961
|
-
/* @__PURE__ */
|
|
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
|
|
3142
|
-
import
|
|
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 = `${
|
|
4224
|
+
const platform = `${os6.type()} ${os6.release()}`;
|
|
3147
4225
|
const nodeVersion = process.version;
|
|
3148
4226
|
const workspace = process.cwd();
|
|
3149
|
-
const workspaceName =
|
|
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(
|
|
4794
|
+
const { waitUntilExit, cleanup } = render(React8.createElement(Root), {
|
|
3717
4795
|
patchConsole: false,
|
|
3718
4796
|
exitOnCtrlC: true
|
|
3719
4797
|
});
|