sharkit 0.2.0.dev1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/PKG-INFO +2 -2
  2. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/README.md +1 -1
  3. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/pyproject.toml +1 -1
  4. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/app.py +2 -2
  5. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/builtins.py +114 -24
  6. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/prompt.py +8 -3
  7. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/repl.py +10 -3
  8. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/renderer.py +27 -7
  9. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/manager.py +15 -1
  10. {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.1/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +17 -5
  11. sharkit-0.2.1/src/sharkit/tools/osint/recon/username/maigret.py +252 -0
  12. sharkit-0.2.1/src/sharkit/tools/osint/recon/username/sherlock.py +252 -0
  13. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/registry.py +14 -0
  14. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/.gitignore +0 -0
  15. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/LICENSE +0 -0
  16. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/__init__.py +0 -0
  17. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/__main__.py +0 -0
  18. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/__init__.py +0 -0
  19. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/base.py +0 -0
  20. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/commands/registry.py +0 -0
  21. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/__init__.py +0 -0
  22. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/manager.py +0 -0
  23. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/config/paths.py +0 -0
  24. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/__init__.py +0 -0
  25. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/console/completion.py +0 -0
  26. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/exceptions.py +0 -0
  27. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/history/__init__.py +0 -0
  28. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/history/manager.py +0 -0
  29. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/network/__init__.py +0 -0
  30. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/network/client.py +0 -0
  31. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/__init__.py +0 -0
  32. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/output/theme.py +0 -0
  33. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/__init__.py +0 -0
  34. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/base.py +0 -0
  35. {sharkit-0.2.0.dev1 → sharkit-0.2.1}/src/sharkit/tools/loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sharkit
3
- Version: 0.2.0.dev1
3
+ Version: 0.2.1
4
4
  Summary: OSINT / HUMINT / GEOINT research framework
5
5
  Project-URL: Homepage, https://github.com/sshsrv/sharkit
6
6
  Project-URL: Repository, https://github.com/sshsrv/sharkit
@@ -27,7 +27,7 @@ Requires-Dist: prompt-toolkit<4.0.0,>=3.0.0
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
30
-
30
+ <br>
31
31
  <div align="center">
32
32
 
33
33
  ![PyPI](https://img.shields.io/pypi/v/sharkit?labelColor=000000&color=ff5faf&style=flat)
@@ -1,5 +1,5 @@
1
1
  <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
2
-
2
+ <br>
3
3
  <div align="center">
4
4
 
5
5
  ![PyPI](https://img.shields.io/pypi/v/sharkit?labelColor=000000&color=ff5faf&style=flat)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sharkit"
3
- version = "0.2.0.dev1"
3
+ version = "0.2.1"
4
4
  description = "OSINT / HUMINT / GEOINT research framework"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,7 +20,7 @@ NON_LINUX_MESSAGE = (
20
20
  "it was built for open water,\n"
21
21
  "where it can actually swim.\n"
22
22
  "\n"
23
- "windows/mac is more of a glass aquarium.\n"
23
+ "windows is more of a glass aquarium.\n"
24
24
  "tidy, bright, and a little too enclosed.\n"
25
25
  "\n"
26
26
  "the shark doesn't dislike the aquarium.\n"
@@ -37,7 +37,7 @@ def main() -> int:
37
37
  sys.stdout.flush()
38
38
  renderer = Renderer()
39
39
 
40
- if sys.platform != "linux":
40
+ if sys.platform == "win32":
41
41
  renderer.banner(tool_count=0, message=NON_LINUX_MESSAGE)
42
42
  return 1
43
43
 
@@ -187,15 +187,22 @@ class UseCommand(Command):
187
187
  if tool_cls is not None and tool_cls.metadata.install is not None:
188
188
  spec = tool_cls.metadata.install
189
189
  name = tool_cls.metadata.name
190
+ display = tool_registry.format_display(path) if tool_registry else name
190
191
  if tool_manager is not None and not tool_manager.is_installed(name):
191
192
  if not self._prompt_install(name):
192
193
  self._log(context, f'Tool "{name}" is not installed. Run: install {name}')
193
194
  return None
194
195
  renderer = context.session.get("renderer")
196
+ first_progress = [True]
195
197
 
196
198
  def on_progress(msg: str) -> None:
197
199
  if renderer is not None:
198
- renderer.log_line(f"install ({name})", msg, color=GREEN)
200
+ tag = f"install {display}"
201
+ if first_progress[0]:
202
+ renderer.gutter(tag, msg, GREEN, is_first=True)
203
+ first_progress[0] = False
204
+ else:
205
+ renderer.gutter(tag, msg, GREEN, is_first=False)
199
206
 
200
207
  ok, message = tool_manager.install(name, spec, on_progress=on_progress)
201
208
  if not ok:
@@ -203,6 +210,15 @@ class UseCommand(Command):
203
210
  return None
204
211
  context.current_tool = path
205
212
  self._log(context, f'Tool "{path}" selected.')
213
+ if tool_cls is not None and tool_cls.metadata.install is not None:
214
+ renderer = context.session.get("renderer")
215
+ if renderer is not None:
216
+ install_spec = tool_cls.metadata.install
217
+ renderer.support_creator(
218
+ tool_cls.metadata.author,
219
+ tool_cls.metadata.name,
220
+ install_spec.git_url,
221
+ )
206
222
  return None
207
223
 
208
224
  def _prompt_install(self, name: str) -> bool:
@@ -372,7 +388,13 @@ class SetCommand(Command):
372
388
  tool_instance.set_option(key, value)
373
389
  renderer = context.session.get("renderer")
374
390
  if renderer is not None:
375
- renderer.log_line(f"set ({context.current_tool})", f"{key} = {value}")
391
+ tool_registry = context.session.get("tool_registry")
392
+ display = context.current_tool
393
+ if tool_registry is not None and context.current_tool is not None:
394
+ display = tool_registry.format_display(
395
+ tool_registry.get_tool_path(context.current_tool),
396
+ )
397
+ renderer.log_line(f"set {display}", f"{key} = {value}")
376
398
  return None
377
399
  return f"Set {key} = {value}"
378
400
  except Exception as e:
@@ -552,6 +574,9 @@ class HistoryCommand(Command):
552
574
  if clear:
553
575
  manager.clear()
554
576
  context.session["history"] = []
577
+ sharkit_history = context.session.get("sharkit_history")
578
+ if sharkit_history is not None:
579
+ sharkit_history.clear()
555
580
  return "[+] Command history and history file cleared"
556
581
 
557
582
  all_entries = manager.get_history(10_000_000)
@@ -614,23 +639,44 @@ class InstallCommand(Command):
614
639
  tool_cls = tool_registry.get_tool(name)
615
640
  if tool_cls is None or tool_cls.metadata.install is None:
616
641
  return f'Tool "{name}" is not an external tool or was not found.'
617
- if tool_manager.is_installed(name):
642
+ tool_name = tool_cls.metadata.name
643
+ display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
644
+ if tool_manager.is_installed(tool_name):
618
645
  renderer = context.session.get("renderer")
619
646
  if renderer is not None:
620
- renderer.log_line(f"install ({name})", "already installed", color=GREEN)
647
+ renderer.log_line(f"install {display}", "already installed", color=GREEN)
621
648
  return None
622
649
  renderer = context.session.get("renderer")
650
+ first_progress = [True]
623
651
 
624
652
  def on_progress(msg: str) -> None:
625
653
  if renderer is not None:
626
- renderer.log_line(f"install ({name})", msg, color=GREEN)
654
+ tag = f"install {display}"
655
+ if first_progress[0]:
656
+ renderer.gutter(tag, msg, GREEN, is_first=True)
657
+ first_progress[0] = False
658
+ else:
659
+ renderer.gutter(tag, msg, GREEN, is_first=False)
627
660
 
628
- ok, message = tool_manager.install(name, tool_cls.metadata.install, on_progress=on_progress)
661
+ install_spec = tool_cls.metadata.install
662
+ ok, message = tool_manager.install(
663
+ tool_name, install_spec, on_progress=on_progress
664
+ )
629
665
  if renderer is not None:
666
+ tag = f"install {display}"
630
667
  if ok:
631
- renderer.log_line(f"install ({name})", "installed", color=GREEN)
668
+ msg = "installed"
669
+ elif message == "Aborted":
670
+ msg = "aborted"
671
+ print("\r\033[K", end="")
672
+ else:
673
+ msg = f"failed: {message}"
674
+ if first_progress[0]:
675
+ renderer.log_line(tag, msg, color=GREEN)
632
676
  else:
633
- renderer.log_line(f"install ({name})", f"failed: {message}", color=GREEN)
677
+ lines = msg.splitlines() or [""]
678
+ for line in lines:
679
+ renderer.gutter(tag, line, GREEN, is_first=False)
634
680
  return None
635
681
  return str(message)
636
682
 
@@ -657,23 +703,44 @@ class UpgradeCommand(Command):
657
703
  tool_cls = tool_registry.get_tool(name)
658
704
  if tool_cls is None or tool_cls.metadata.install is None:
659
705
  return f'Tool "{name}" is not an external tool or was not found.'
660
- if not tool_manager.is_installed(name):
706
+ tool_name = tool_cls.metadata.name
707
+ display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
708
+ if not tool_manager.is_installed(tool_name):
661
709
  renderer = context.session.get("renderer")
662
710
  if renderer is not None:
663
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
711
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
664
712
  return None
665
713
  renderer = context.session.get("renderer")
714
+ first_progress = [True]
666
715
 
667
716
  def on_progress(msg: str) -> None:
668
717
  if renderer is not None:
669
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
718
+ tag = f"upgrade {display}"
719
+ if first_progress[0]:
720
+ renderer.gutter(tag, msg, BLUE, is_first=True)
721
+ first_progress[0] = False
722
+ else:
723
+ renderer.gutter(tag, msg, BLUE, is_first=False)
670
724
 
671
- ok, message = tool_manager.update(name, tool_cls.metadata.install, on_progress=on_progress)
725
+ upgrade_spec = tool_cls.metadata.install
726
+ ok, message = tool_manager.update(
727
+ tool_name, upgrade_spec, on_progress=on_progress
728
+ )
672
729
  if renderer is not None:
730
+ tag = f"upgrade {display}"
673
731
  if ok:
674
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
732
+ msg = "upgraded"
733
+ elif message == "Aborted":
734
+ msg = "aborted"
735
+ print("\r\033[K", end="")
736
+ else:
737
+ msg = f"failed: {message}"
738
+ if first_progress[0]:
739
+ renderer.log_line(tag, msg, color=BLUE)
675
740
  else:
676
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
741
+ lines = msg.splitlines() or [""]
742
+ for line in lines:
743
+ renderer.gutter(tag, line, BLUE, is_first=False)
677
744
  return None
678
745
  return str(message)
679
746
 
@@ -699,23 +766,44 @@ class UpgradeCommand(Command):
699
766
  if spec is None:
700
767
  continue
701
768
  name = meta.name
769
+ display = tool_registry.format_display(tool_registry.get_tool_path(name))
702
770
  if not tool_manager.is_installed(name):
703
771
  renderer = context.session.get("renderer")
704
772
  if renderer is not None:
705
- renderer.log_line(f"upgrade ({name})", "not installed (skipped)", color=BLUE)
773
+ renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
706
774
  continue
707
775
  renderer = context.session.get("renderer")
708
-
709
- def on_progress(msg: str, name: str = name, renderer: Any = renderer) -> None:
710
- if renderer is not None:
711
- renderer.log_line(f"upgrade ({name})", msg, color=BLUE)
776
+ first_progress = [True]
777
+
778
+ def on_progress(
779
+ msg: str,
780
+ _first: list[bool] = first_progress,
781
+ _tag: str = f"upgrade {display}",
782
+ _renderer: Any = renderer,
783
+ ) -> None:
784
+ if _renderer is not None:
785
+ if _first[0]:
786
+ _renderer.gutter(_tag, msg, BLUE, is_first=True)
787
+ _first[0] = False
788
+ else:
789
+ _renderer.gutter(_tag, msg, BLUE, is_first=False)
712
790
 
713
791
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
714
792
  if renderer is not None:
793
+ tag = f"upgrade {display}"
715
794
  if ok:
716
- renderer.log_line(f"upgrade ({name})", "upgraded", color=BLUE)
795
+ msg = "upgraded"
796
+ elif message == "Aborted":
797
+ msg = "aborted"
798
+ print("\r\033[K", end="")
799
+ else:
800
+ msg = f"failed: {message}"
801
+ if first_progress[0]:
802
+ renderer.log_line(tag, msg, color=BLUE)
717
803
  else:
718
- renderer.log_line(f"upgrade ({name})", f"failed: {message}", color=BLUE)
804
+ lines = msg.splitlines() or [""]
805
+ for line in lines:
806
+ renderer.gutter(tag, line, BLUE, is_first=False)
719
807
  return None
720
808
 
721
809
 
@@ -737,13 +825,15 @@ class RemoveCommand(Command):
737
825
  tool_cls = tool_registry.get_tool(name)
738
826
  if tool_cls is None or tool_cls.metadata.install is None:
739
827
  return f'Tool "{name}" is not an external tool or was not found.'
828
+ tool_name = tool_cls.metadata.name
829
+ display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
740
830
  renderer = context.session.get("renderer")
741
- ok, message = tool_manager.uninstall(name)
831
+ ok, message = tool_manager.uninstall(tool_name)
742
832
  if renderer is not None:
743
833
  if ok:
744
- renderer.log_line(f"remove ({name})", "removed", color=RED)
834
+ renderer.log_line(f"remove {display}", "removed", color=RED)
745
835
  else:
746
- renderer.log_line(f"remove ({name})", f"failed: {message}", color=RED)
836
+ renderer.log_line(f"remove {display}", f"failed: {message}", color=RED)
747
837
  return None
748
838
  return str(message)
749
839
 
@@ -9,16 +9,21 @@ class PromptRenderer:
9
9
  return "shark > "
10
10
 
11
11
  def tool_prompt(self, tool_name: str) -> str:
12
- return f"shark ({tool_name}) > "
12
+ if "/" in tool_name:
13
+ parts = tool_name.split("/", 1)
14
+ formatted = f"{parts[0]}({parts[1]})"
15
+ else:
16
+ formatted = tool_name
17
+ return f"shark {formatted} > "
13
18
 
14
19
  def render(self, prompt: str) -> list[tuple[str, str]]:
15
20
  if "(" in prompt and ")" in prompt:
16
21
  before, rest = prompt.split("(", 1)
17
- tool_name, after = rest.split(")", 1)
22
+ tool_path, after = rest.split(")", 1)
18
23
  return [
19
24
  (PROMPT_STYLE, before),
20
25
  ("", "("),
21
- (MODULE_STYLE, tool_name),
26
+ (MODULE_STYLE, tool_path),
22
27
  ("", ")"),
23
28
  ("", after),
24
29
  ]
@@ -36,6 +36,11 @@ class _SharkitHistory(History):
36
36
  def store_string(self, string: str) -> None:
37
37
  self._manager.add_command(string)
38
38
 
39
+ def clear(self) -> None:
40
+ """Reset the in-memory buffer so next load() re-reads from file."""
41
+ self._loaded = False
42
+ self._loaded_strings = []
43
+
39
44
 
40
45
  class _ConsoleCompleter(Completer):
41
46
  def __init__(
@@ -100,9 +105,9 @@ class Console:
100
105
 
101
106
  def _get_prompt(self) -> Any:
102
107
  if self._current_tool is not None:
103
- prompt = f"shark ({self._current_tool}) > "
108
+ prompt = self._prompt_renderer.tool_prompt(self._current_tool)
104
109
  else:
105
- prompt = "shark > "
110
+ prompt = self._prompt_renderer.default_prompt()
106
111
  return self._prompt_renderer.render(prompt)
107
112
 
108
113
  def _init_session(self) -> None:
@@ -113,12 +118,14 @@ class Console:
113
118
  tool_registry=self._tool_registry,
114
119
  get_current_tool=lambda: self._current_tool,
115
120
  )
121
+ self._sharkit_history = _SharkitHistory(self._history_manager)
116
122
  self._prompt_session = PromptSession[str](
117
- history=_SharkitHistory(self._history_manager),
123
+ history=self._sharkit_history,
118
124
  completer=self._completer,
119
125
  style=_PROMPT_STYLE,
120
126
  complete_while_typing=True,
121
127
  )
128
+ self._session_data["sharkit_history"] = self._sharkit_history
122
129
 
123
130
  def run(self) -> None:
124
131
  self._init_session()
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import shutil
3
3
  import textwrap
4
+ import unicodedata
4
5
 
5
6
  from sharkit import __version__
6
7
  from sharkit.output.theme import (
@@ -32,7 +33,8 @@ x88: `)8b. 888E~?888L .@88 "8888" 4888>'88" 888E`"88*" ''888E` 8888
32
33
 
33
34
 
34
35
  def _visible_len(text: str) -> int:
35
- return len(ANSI_RE.sub("", text))
36
+ clean = ANSI_RE.sub("", text)
37
+ return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in clean)
36
38
 
37
39
 
38
40
  def _pad(text: str, width: int) -> str:
@@ -55,7 +57,7 @@ def _term_width() -> int:
55
57
  class Renderer:
56
58
  def panel(
57
59
  self,
58
- title: str,
60
+ title: str | None,
59
61
  content: str,
60
62
  min_width: int = 0,
61
63
  centered: bool = False,
@@ -73,11 +75,12 @@ class Renderer:
73
75
  else:
74
76
  lines = content.splitlines()
75
77
  content_width = max((_visible_len(line) for line in lines), default=0)
76
- title_width = _visible_len(title)
78
+ title_width = _visible_len(title) if title else 0
77
79
  inner = max(content_width, title_width + 3, min_width)
78
- box_width = inner + 4
79
80
 
80
- if centered:
81
+ if title is None:
82
+ top = f"{PINK}╭{'─' * (inner + 2)}╮{RESET}"
83
+ elif centered:
81
84
  gap = inner - title_width
82
85
  left = gap // 2
83
86
  right = gap - left
@@ -88,6 +91,7 @@ class Renderer:
88
91
  else:
89
92
  dashes = max(0, inner - title_width - 1)
90
93
  top = f"{PINK}╭─ {BOLD}{WHITE}{title}{RESET}{PINK} {'─' * dashes}╮{RESET}"
94
+
91
95
  if centered:
92
96
  body = [f"{PINK}│{RESET} {_center(line, inner)} {PINK}│{RESET}" for line in lines]
93
97
  else:
@@ -96,7 +100,7 @@ class Renderer:
96
100
 
97
101
  all_lines = [top, *body, bottom]
98
102
  if centered:
99
- pad = max(0, (_term_width() - box_width) // 2)
103
+ pad = max(0, (_term_width() - (inner + 4)) // 2)
100
104
  all_lines = [" " * pad + ln for ln in all_lines]
101
105
  for ln in all_lines:
102
106
  print(ln)
@@ -249,7 +253,10 @@ class Renderer:
249
253
  print(text, end="")
250
254
 
251
255
  def log_line(self, tag: str, message: str, color: str = PINK) -> None:
252
- print(f"{color}{BOLD}{tag} - {RESET}{message}")
256
+ lines = message.splitlines() or [""]
257
+ self.gutter(tag, lines[0], color, is_first=True)
258
+ for line in lines[1:]:
259
+ print(f"{color}{BOLD}│{RESET} {line}")
253
260
 
254
261
  def gutter(self, name: str, line: str, color: str, is_first: bool) -> None:
255
262
  if is_first:
@@ -257,3 +264,16 @@ class Renderer:
257
264
  else:
258
265
  prefix = f"{RESET}{' ' * (len(name) + 1)}{color}{BOLD}│{RESET} "
259
266
  print(prefix + line)
267
+
268
+ def support_creator(self, author: str, tool_name: str, git_url: str) -> None:
269
+ lines = [
270
+ f"Thanks to {author} for building {tool_name}!",
271
+ "If you find this tool useful, please consider:",
272
+ "",
273
+ f"⭐ Star the repo: {git_url}",
274
+ f"🐛 Report issues: {git_url}/issues",
275
+ f"💰 Sponsor the author: {git_url}/sponsors",
276
+ "",
277
+ f"{DIM}Without creators like them, sharkit wouldn't exist! :3{RESET}",
278
+ ]
279
+ self.panel(None, "\n".join(lines))
@@ -54,7 +54,11 @@ class ToolManager:
54
54
  self._run([pip, "install", *spec.pip_args])
55
55
  dest.joinpath(".installed").write_text(name)
56
56
  return True, f'Tool "{name}" installed.'
57
+ except KeyboardInterrupt:
58
+ shutil.rmtree(dest, ignore_errors=True)
59
+ return False, "Aborted"
57
60
  except Exception as e:
61
+ shutil.rmtree(dest, ignore_errors=True)
58
62
  return False, f'Failed to install "{name}": {e}'
59
63
 
60
64
  def update(
@@ -82,6 +86,8 @@ class ToolManager:
82
86
  on_progress("upgrading packages...")
83
87
  self._run([pip, "install", "--upgrade", *spec.pip_args])
84
88
  return True, f'Tool "{name}" updated.'
89
+ except KeyboardInterrupt:
90
+ return False, "Aborted"
85
91
  except Exception as e:
86
92
  return False, f'Failed to update "{name}": {e}'
87
93
 
@@ -96,4 +102,12 @@ class ToolManager:
96
102
  return False, f'Failed to uninstall "{name}": {e}'
97
103
 
98
104
  def _run(self, cmd: list[str]) -> None:
99
- subprocess.run(cmd, check=True, capture_output=True)
105
+ result = subprocess.run(cmd, capture_output=True, text=True)
106
+ if result.returncode != 0:
107
+ stderr = result.stderr.strip() if result.stderr else ""
108
+ stdout = result.stdout.strip() if result.stdout else ""
109
+ raw = stderr or stdout or "No output"
110
+ # Truncate long build output — keep last 20 lines where the actual error lives
111
+ lines = raw.splitlines()
112
+ detail = "\n".join(lines[-20:]) if len(lines) > 20 else raw
113
+ raise RuntimeError(detail)
@@ -107,7 +107,7 @@ class BlackbirdTool(Tool):
107
107
  success=False,
108
108
  error="Both 'type' and 'query' options are required.",
109
109
  )
110
- args = [f"--{stype}", query, "--no-update"]
110
+ args = [f"--{stype}", query]
111
111
  if parse_bool(context.options.get("pdf")):
112
112
  args.append("--pdf")
113
113
  renderer = context.renderer
@@ -144,6 +144,7 @@ class BlackbirdTool(Tool):
144
144
  first = True
145
145
  current = ""
146
146
  started = False
147
+ pending_blank = False
147
148
  try:
148
149
  while True:
149
150
  try:
@@ -165,11 +166,20 @@ class BlackbirdTool(Tool):
165
166
  started = True
166
167
  for ch in chunk.decode("utf-8", errors="replace"):
167
168
  if ch == "\n":
168
- if renderer is not None:
169
- renderer.gutter(name, _clean_ansi(current), tool_color, first)
169
+ if current:
170
+ if pending_blank:
171
+ if renderer is not None:
172
+ renderer.gutter(name, "", tool_color, False)
173
+ else:
174
+ print()
175
+ pending_blank = False
176
+ if renderer is not None:
177
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
178
+ else:
179
+ print(_clean_ansi(current))
180
+ first = False
170
181
  else:
171
- print(_clean_ansi(current))
172
- first = False
182
+ pending_blank = True
173
183
  current = ""
174
184
  elif ch == "\r":
175
185
  current = ""
@@ -181,6 +191,8 @@ class BlackbirdTool(Tool):
181
191
  proc.kill()
182
192
  current = ""
183
193
  if renderer is not None:
194
+ # \r\033[K clears the ^C echo from the terminal before printing.
195
+ print("\r\033[K", end="")
184
196
  renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
185
197
  else:
186
198
  print("Run aborted (Ctrl+C).")
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import fcntl
5
+ import os
6
+ import pty
7
+ import re
8
+ import select
9
+ import struct
10
+ import subprocess
11
+ import termios
12
+ from dataclasses import replace
13
+
14
+ from sharkit.output.theme import PINK, hex_to_ansi
15
+ from sharkit.tools.base import (
16
+ ExecutionContext,
17
+ OptionDefinition,
18
+ Result,
19
+ Tool,
20
+ ToolInstallSpec,
21
+ ToolMetadata,
22
+ parse_bool,
23
+ )
24
+
25
+ _CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
26
+ _OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
27
+
28
+
29
+ def _clean_ansi(text: str) -> str:
30
+ text = _OSC_RE.sub("", text)
31
+ return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
32
+
33
+
34
+ class MaigretTool(Tool):
35
+ metadata = ToolMetadata(
36
+ name="maigret",
37
+ description="Maigret — username OSINT across 3000+ sites with recursive checks",
38
+ category="recon",
39
+ author="soxoj",
40
+ version="0.1.0",
41
+ safety="safe",
42
+ color="#FF6B35",
43
+ install=ToolInstallSpec(
44
+ git_url="https://github.com/soxoj/maigret.git",
45
+ pip_args=["maigret"],
46
+ ),
47
+ )
48
+
49
+ def __init__(self) -> None:
50
+ self._options = {
51
+ "username": OptionDefinition(
52
+ name="username",
53
+ description="Target username to hunt",
54
+ required=True,
55
+ ),
56
+ "timeout": OptionDefinition(
57
+ name="timeout",
58
+ description="Timeout per site in seconds",
59
+ required=False,
60
+ default=None,
61
+ ),
62
+ "output": OptionDefinition(
63
+ name="output",
64
+ description="Write results to file",
65
+ required=False,
66
+ default=None,
67
+ ),
68
+ "json": OptionDefinition(
69
+ name="json",
70
+ description="Export results as JSON",
71
+ required=False,
72
+ default=None,
73
+ type="bool",
74
+ ),
75
+ "csv": OptionDefinition(
76
+ name="csv",
77
+ description="Export results as CSV",
78
+ required=False,
79
+ default=None,
80
+ type="bool",
81
+ ),
82
+ "site": OptionDefinition(
83
+ name="site",
84
+ description="Limit to specific site(s) (comma-separated)",
85
+ required=False,
86
+ default=None,
87
+ ),
88
+ "top_sites": OptionDefinition(
89
+ name="top_sites",
90
+ description="Only check top N most popular sites",
91
+ required=False,
92
+ default=None,
93
+ ),
94
+ }
95
+
96
+ def get_metadata(self) -> ToolMetadata:
97
+ return self.metadata
98
+
99
+ def get_options(self) -> dict[str, OptionDefinition]:
100
+ return self._options
101
+
102
+ def set_option(self, key: str, value: str) -> None:
103
+ if key not in self._options:
104
+ raise ValueError(f'Option "{key}" not found.')
105
+ self._options[key] = replace(self._options[key], value=value)
106
+
107
+ def execute(self, context: ExecutionContext) -> Result:
108
+ from sharkit.tools.manager import ToolManager
109
+
110
+ name = self.metadata.name
111
+ tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
112
+ manager = ToolManager()
113
+ if not manager.is_installed(name):
114
+ return Result(
115
+ success=False,
116
+ error=f'Tool "{name}" is not installed. Run: install {name}',
117
+ )
118
+ spec = self.metadata.install
119
+ if spec is None:
120
+ return Result(success=False, error="Tool has no install spec.")
121
+
122
+ install_dir = manager.install_path(name)
123
+ maigret_bin = install_dir / "venv" / "bin" / "maigret"
124
+ if not maigret_bin.exists():
125
+ return Result(
126
+ success=False,
127
+ error=f"Maigret binary not found: {maigret_bin}",
128
+ )
129
+
130
+ username = context.options.get("username") or ""
131
+ if not username:
132
+ return Result(success=False, error="Option 'username' is required.")
133
+
134
+ args = [username, "--no-progressbar"]
135
+ timeout = context.options.get("timeout")
136
+ if timeout:
137
+ args.extend(["--timeout", str(timeout)])
138
+ output = context.options.get("output")
139
+ if output:
140
+ args.extend(["--output", str(output)])
141
+ if parse_bool(context.options.get("json")):
142
+ args.append("--json")
143
+ if parse_bool(context.options.get("csv")):
144
+ args.append("--csv")
145
+ site = context.options.get("site")
146
+ if site:
147
+ for s in str(site).split(","):
148
+ args.extend(["--site", s.strip()])
149
+ top_sites = context.options.get("top_sites")
150
+ if top_sites:
151
+ args.extend(["--top-sites", str(top_sites)])
152
+
153
+ renderer = context.renderer
154
+ master, slave = pty.openpty()
155
+ try:
156
+ winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
157
+ fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
158
+ attrs = termios.tcgetattr(master)
159
+ attrs[1] &= ~termios.OPOST
160
+ termios.tcsetattr(master, termios.TCSANOW, attrs)
161
+ except OSError:
162
+ pass
163
+
164
+ env = os.environ.copy()
165
+ env["TERM"] = "xterm-256color"
166
+ env["FORCE_COLOR"] = "1"
167
+ env["CLICOLOR"] = "1"
168
+ env["CLICOLOR_FORCE"] = "1"
169
+ env["COLUMNS"] = "10000"
170
+
171
+ try:
172
+ proc = subprocess.Popen(
173
+ [str(maigret_bin), *args],
174
+ stdout=slave,
175
+ stderr=slave,
176
+ stdin=subprocess.DEVNULL,
177
+ cwd=str(install_dir),
178
+ env=env,
179
+ start_new_session=True,
180
+ )
181
+ except Exception as e:
182
+ return Result(success=False, error=f"Failed to run {name}: {e}")
183
+ finally:
184
+ with contextlib.suppress(OSError):
185
+ os.close(slave)
186
+
187
+ first = True
188
+ current = ""
189
+ started = False
190
+ pending_blank = False
191
+ try:
192
+ while True:
193
+ try:
194
+ r, _, _ = select.select([master], [], [], 0.5)
195
+ except (OSError, ValueError):
196
+ break
197
+ if not r:
198
+ if proc.poll() is not None:
199
+ break
200
+ continue
201
+ try:
202
+ chunk = os.read(master, 65536)
203
+ except OSError:
204
+ break
205
+ if not chunk:
206
+ break
207
+ if not started:
208
+ chunk = chunk.lstrip(b"\n")
209
+ started = True
210
+ for ch in chunk.decode("utf-8", errors="replace"):
211
+ if ch == "\n":
212
+ if current:
213
+ if pending_blank:
214
+ if renderer is not None:
215
+ renderer.gutter(name, "", tool_color, False)
216
+ else:
217
+ print()
218
+ pending_blank = False
219
+ if renderer is not None:
220
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
221
+ else:
222
+ print(_clean_ansi(current))
223
+ first = False
224
+ else:
225
+ pending_blank = True
226
+ current = ""
227
+ elif ch == "\r":
228
+ current = ""
229
+ else:
230
+ current += ch
231
+ except KeyboardInterrupt:
232
+ with contextlib.suppress(OSError):
233
+ proc.kill()
234
+ current = ""
235
+ if renderer is not None:
236
+ print("\r\033[K", end="")
237
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
238
+ else:
239
+ print("Run aborted (Ctrl+C).")
240
+ return Result(success=True, data={})
241
+ finally:
242
+ if current:
243
+ if renderer is not None:
244
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
245
+ else:
246
+ print(_clean_ansi(current))
247
+ with contextlib.suppress(OSError):
248
+ os.close(master)
249
+ with contextlib.suppress(OSError):
250
+ proc.wait()
251
+
252
+ return Result(success=True, data={})
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import fcntl
5
+ import os
6
+ import pty
7
+ import re
8
+ import select
9
+ import struct
10
+ import subprocess
11
+ import termios
12
+ from dataclasses import replace
13
+
14
+ from sharkit.output.theme import PINK, hex_to_ansi
15
+ from sharkit.tools.base import (
16
+ ExecutionContext,
17
+ OptionDefinition,
18
+ Result,
19
+ Tool,
20
+ ToolInstallSpec,
21
+ ToolMetadata,
22
+ parse_bool,
23
+ )
24
+
25
+ _CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
26
+ _OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
27
+
28
+
29
+ def _clean_ansi(text: str) -> str:
30
+ text = _OSC_RE.sub("", text)
31
+ return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
32
+
33
+
34
+ class SherlockTool(Tool):
35
+ metadata = ToolMetadata(
36
+ name="sherlock",
37
+ description="Sherlock — find usernames across 400+ social networks",
38
+ category="recon",
39
+ author="sherlock-project",
40
+ version="0.1.0",
41
+ safety="safe",
42
+ color="#4A7AFF",
43
+ install=ToolInstallSpec(
44
+ git_url="https://github.com/sherlock-project/sherlock.git",
45
+ pip_args=["sherlock-project"],
46
+ ),
47
+ )
48
+
49
+ def __init__(self) -> None:
50
+ self._options = {
51
+ "username": OptionDefinition(
52
+ name="username",
53
+ description="Target username to hunt",
54
+ required=True,
55
+ ),
56
+ "timeout": OptionDefinition(
57
+ name="timeout",
58
+ description="Timeout per site in seconds",
59
+ required=False,
60
+ default=None,
61
+ ),
62
+ "output": OptionDefinition(
63
+ name="output",
64
+ description="Write results to file",
65
+ required=False,
66
+ default=None,
67
+ ),
68
+ "json": OptionDefinition(
69
+ name="json",
70
+ description="Export results as JSON",
71
+ required=False,
72
+ default=None,
73
+ type="bool",
74
+ ),
75
+ "csv": OptionDefinition(
76
+ name="csv",
77
+ description="Export results as CSV",
78
+ required=False,
79
+ default=None,
80
+ type="bool",
81
+ ),
82
+ "site": OptionDefinition(
83
+ name="site",
84
+ description="Limit to specific site(s) (comma-separated)",
85
+ required=False,
86
+ default=None,
87
+ ),
88
+ "print_found": OptionDefinition(
89
+ name="print_found",
90
+ description="Only print sites where the username was found",
91
+ required=False,
92
+ default=None,
93
+ type="bool",
94
+ ),
95
+ }
96
+
97
+ def get_metadata(self) -> ToolMetadata:
98
+ return self.metadata
99
+
100
+ def get_options(self) -> dict[str, OptionDefinition]:
101
+ return self._options
102
+
103
+ def set_option(self, key: str, value: str) -> None:
104
+ if key not in self._options:
105
+ raise ValueError(f'Option "{key}" not found.')
106
+ self._options[key] = replace(self._options[key], value=value)
107
+
108
+ def execute(self, context: ExecutionContext) -> Result:
109
+ from sharkit.tools.manager import ToolManager
110
+
111
+ name = self.metadata.name
112
+ tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
113
+ manager = ToolManager()
114
+ if not manager.is_installed(name):
115
+ return Result(
116
+ success=False,
117
+ error=f'Tool "{name}" is not installed. Run: install {name}',
118
+ )
119
+ spec = self.metadata.install
120
+ if spec is None:
121
+ return Result(success=False, error="Tool has no install spec.")
122
+
123
+ install_dir = manager.install_path(name)
124
+ sherlock_bin = install_dir / "venv" / "bin" / "sherlock"
125
+ if not sherlock_bin.exists():
126
+ return Result(
127
+ success=False,
128
+ error=f"Sherlock binary not found: {sherlock_bin}",
129
+ )
130
+
131
+ username = context.options.get("username") or ""
132
+ if not username:
133
+ return Result(success=False, error="Option 'username' is required.")
134
+
135
+ args = [username]
136
+ timeout = context.options.get("timeout")
137
+ if timeout:
138
+ args.extend(["--timeout", str(timeout)])
139
+ output = context.options.get("output")
140
+ if output:
141
+ args.extend(["--output", str(output)])
142
+ if parse_bool(context.options.get("json")):
143
+ args.append("--json")
144
+ if parse_bool(context.options.get("csv")):
145
+ args.append("--csv")
146
+ site = context.options.get("site")
147
+ if site:
148
+ for s in str(site).split(","):
149
+ args.extend(["--site", s.strip()])
150
+ if parse_bool(context.options.get("print_found")):
151
+ args.append("--print-found")
152
+
153
+ renderer = context.renderer
154
+ master, slave = pty.openpty()
155
+ try:
156
+ winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
157
+ fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
158
+ attrs = termios.tcgetattr(master)
159
+ attrs[1] &= ~termios.OPOST
160
+ termios.tcsetattr(master, termios.TCSANOW, attrs)
161
+ except OSError:
162
+ pass
163
+
164
+ env = os.environ.copy()
165
+ env["TERM"] = "xterm-256color"
166
+ env["FORCE_COLOR"] = "1"
167
+ env["CLICOLOR"] = "1"
168
+ env["CLICOLOR_FORCE"] = "1"
169
+ env["COLUMNS"] = "10000"
170
+
171
+ try:
172
+ proc = subprocess.Popen(
173
+ [str(sherlock_bin), *args],
174
+ stdout=slave,
175
+ stderr=slave,
176
+ stdin=subprocess.DEVNULL,
177
+ cwd=str(install_dir),
178
+ env=env,
179
+ start_new_session=True,
180
+ )
181
+ except Exception as e:
182
+ return Result(success=False, error=f"Failed to run {name}: {e}")
183
+ finally:
184
+ with contextlib.suppress(OSError):
185
+ os.close(slave)
186
+
187
+ first = True
188
+ current = ""
189
+ started = False
190
+ pending_blank = False
191
+ try:
192
+ while True:
193
+ try:
194
+ r, _, _ = select.select([master], [], [], 0.5)
195
+ except (OSError, ValueError):
196
+ break
197
+ if not r:
198
+ if proc.poll() is not None:
199
+ break
200
+ continue
201
+ try:
202
+ chunk = os.read(master, 65536)
203
+ except OSError:
204
+ break
205
+ if not chunk:
206
+ break
207
+ if not started:
208
+ chunk = chunk.lstrip(b"\n")
209
+ started = True
210
+ for ch in chunk.decode("utf-8", errors="replace"):
211
+ if ch == "\n":
212
+ if current:
213
+ if pending_blank:
214
+ if renderer is not None:
215
+ renderer.gutter(name, "", tool_color, False)
216
+ else:
217
+ print()
218
+ pending_blank = False
219
+ if renderer is not None:
220
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
221
+ else:
222
+ print(_clean_ansi(current))
223
+ first = False
224
+ else:
225
+ pending_blank = True
226
+ current = ""
227
+ elif ch == "\r":
228
+ current = ""
229
+ else:
230
+ current += ch
231
+ except KeyboardInterrupt:
232
+ with contextlib.suppress(OSError):
233
+ proc.kill()
234
+ current = ""
235
+ if renderer is not None:
236
+ print("\r\033[K", end="")
237
+ renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
238
+ else:
239
+ print("Run aborted (Ctrl+C).")
240
+ return Result(success=True, data={})
241
+ finally:
242
+ if current:
243
+ if renderer is not None:
244
+ renderer.gutter(name, _clean_ansi(current), tool_color, first)
245
+ else:
246
+ print(_clean_ansi(current))
247
+ with contextlib.suppress(OSError):
248
+ os.close(master)
249
+ with contextlib.suppress(OSError):
250
+ proc.wait()
251
+
252
+ return Result(success=True, data={})
@@ -71,3 +71,17 @@ class ToolRegistry:
71
71
  for path, name in self._paths.items():
72
72
  result[path] = self._tools[name]
73
73
  return result
74
+
75
+ def get_tool_path(self, name: str) -> str:
76
+ """Return the full path for a tool name, or just the name if no path."""
77
+ for path, tool_name in self._paths.items():
78
+ if tool_name == name:
79
+ return path
80
+ return name
81
+
82
+ def format_display(self, path: str) -> str:
83
+ """Format tool path as 'category(subpath)' for display."""
84
+ if "/" in path:
85
+ parts = path.split("/", 1)
86
+ return f"{parts[0]}({parts[1]})"
87
+ return path
File without changes
File without changes