sharkit 0.2.1.dev4__tar.gz → 1.0.0.dev1__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 (80) hide show
  1. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/PKG-INFO +1 -1
  2. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/pyproject.toml +1 -1
  3. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/__init__.py +1 -1
  4. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/__main__.py +2 -0
  5. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/app.py +4 -2
  6. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/commands/base.py +3 -3
  7. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/commands/builtins.py +90 -82
  8. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/config/manager.py +7 -9
  9. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/config/paths.py +8 -1
  10. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/console/completion.py +42 -8
  11. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/console/prompt.py +14 -2
  12. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/console/repl.py +2 -4
  13. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/exceptions.py +3 -0
  14. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/history/manager.py +20 -5
  15. sharkit-1.0.0.dev1/src/sharkit/network/client.py +95 -0
  16. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/output/renderer.py +33 -12
  17. sharkit-1.0.0.dev1/src/sharkit/output/theme.py +38 -0
  18. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/base.py +16 -4
  19. sharkit-1.0.0.dev1/src/sharkit/tools/humint/__init__.py +0 -0
  20. sharkit-1.0.0.dev1/src/sharkit/tools/humint/truthcheck.py +195 -0
  21. sharkit-1.0.0.dev1/src/sharkit/tools/humint/verifact.py +188 -0
  22. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/loader.py +5 -1
  23. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/manager.py +22 -8
  24. sharkit-1.0.0.dev1/src/sharkit/tools/osint/ahmia_search.py +74 -0
  25. sharkit-1.0.0.dev1/src/sharkit/tools/osint/email_reputation.py +87 -0
  26. sharkit-1.0.0.dev1/src/sharkit/tools/osint/hibp_passwords.py +83 -0
  27. sharkit-1.0.0.dev1/src/sharkit/tools/osint/humint/mailaccess.py +199 -0
  28. sharkit-1.0.0.dev1/src/sharkit/tools/osint/humint/pyahmia.py +187 -0
  29. sharkit-1.0.0.dev1/src/sharkit/tools/osint/humint/scraperx.py +187 -0
  30. sharkit-1.0.0.dev1/src/sharkit/tools/osint/humint/scweet.py +194 -0
  31. sharkit-1.0.0.dev1/src/sharkit/tools/osint/humint/socialscan.py +198 -0
  32. sharkit-1.0.0.dev1/src/sharkit/tools/osint/leak_check.py +85 -0
  33. sharkit-1.0.0.dev1/src/sharkit/tools/osint/psbdmp.py +75 -0
  34. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/dns/dns_lookup.py +180 -0
  35. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/domain/cve_search.py +155 -0
  36. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/domain/otx_query.py +126 -0
  37. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/domain/urlhaus_check.py +127 -0
  38. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/email/email_security.py +131 -0
  39. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/email/holehe.py +210 -0
  40. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/email/theharvester.py +216 -0
  41. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/osint/recon/hybrid/blackbird.py +12 -9
  42. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/bgpv6.py +94 -0
  43. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/http_headers.py +108 -0
  44. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/ip_geolocate.py +86 -0
  45. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/ip_info.py +76 -0
  46. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/mylnikov.py +79 -0
  47. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/port_scanner.py +140 -0
  48. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/robtex.py +89 -0
  49. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/shodan_internetdb.py +80 -0
  50. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/ssl_check.py +116 -0
  51. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/network/whois_lookup.py +141 -0
  52. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/phone/phoneinfoga.py +210 -0
  53. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/osint/recon/username/maigret.py +12 -9
  54. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/osint/recon/username/sherlock.py +12 -9
  55. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/username/social_analyzer.py +210 -0
  56. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/username/socid_extractor.py +210 -0
  57. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/web/robots_check.py +138 -0
  58. sharkit-1.0.0.dev1/src/sharkit/tools/osint/recon/web/wayback_check.py +122 -0
  59. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/crypto/mempool.py +85 -0
  60. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/hash/circl_hashlookup.py +77 -0
  61. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/hash/hash_decrypt.py +90 -0
  62. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/hash/hash_lookup.py +71 -0
  63. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/image/exif_reader.py +157 -0
  64. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/text/base64_tool.py +79 -0
  65. sharkit-1.0.0.dev1/src/sharkit/tools/osint/util/text/text_entropy.py +81 -0
  66. sharkit-1.0.0.dev1/src/sharkit/tools/osint/xposed_or_not.py +80 -0
  67. sharkit-0.2.1.dev4/src/sharkit/network/client.py +0 -81
  68. sharkit-0.2.1.dev4/src/sharkit/output/theme.py +0 -18
  69. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/.gitignore +0 -0
  70. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/LICENSE +0 -0
  71. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/README.md +0 -0
  72. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/commands/__init__.py +0 -0
  73. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/commands/registry.py +0 -0
  74. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/config/__init__.py +0 -0
  75. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/console/__init__.py +0 -0
  76. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/history/__init__.py +0 -0
  77. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/network/__init__.py +0 -0
  78. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/output/__init__.py +0 -0
  79. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/__init__.py +0 -0
  80. {sharkit-0.2.1.dev4 → sharkit-1.0.0.dev1}/src/sharkit/tools/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sharkit
3
- Version: 0.2.1.dev4
3
+ Version: 1.0.0.dev1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sharkit"
3
- version = "0.2.1.dev4"
3
+ version = "1.0.0.dev1"
4
4
  description = "OSINT / HUMINT / GEOINT research framework"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -5,4 +5,4 @@ try:
5
5
 
6
6
  __version__ = _pkg_version("sharkit")
7
7
  except Exception:
8
- __version__ = "0.2.0.dev1"
8
+ __version__ = "0.3.0.dev1"
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
4
 
3
5
  from sharkit.app import main
@@ -33,8 +33,9 @@ NON_LINUX_MESSAGE = (
33
33
 
34
34
 
35
35
  def main() -> int:
36
- sys.stdout.write("\033[2J\033[3J\033[H")
37
- sys.stdout.flush()
36
+ if sys.stdout.isatty():
37
+ sys.stdout.write("\033[2J\033[3J\033[H")
38
+ sys.stdout.flush()
38
39
  renderer = Renderer()
39
40
 
40
41
  if sys.platform == "win32":
@@ -69,6 +70,7 @@ def main() -> int:
69
70
  history_manager=history_manager,
70
71
  renderer=renderer,
71
72
  )
73
+ console._session_data["http_client"] = http_client
72
74
 
73
75
  try:
74
76
  console.run()
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from dataclasses import dataclass, field
5
- from typing import Any
5
+ from typing import Any, ClassVar
6
6
 
7
7
 
8
8
  @dataclass
@@ -15,10 +15,10 @@ class CommandContext:
15
15
 
16
16
  class Command(ABC):
17
17
  name: str
18
- aliases: list[str] = []
18
+ aliases: ClassVar[list[str]] = []
19
19
  description: str = ""
20
20
  usage: str = ""
21
- options: list[tuple[str, str]] = []
21
+ options: ClassVar[list[tuple[str, str]]] = []
22
22
  notes: str = ""
23
23
 
24
24
  @abstractmethod
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import sys
5
+ from collections.abc import Callable
4
6
  from pathlib import Path
5
7
  from typing import Any
6
8
 
7
9
  from sharkit import __version__
8
10
  from sharkit.commands.base import Command, CommandContext
11
+ from sharkit.exceptions import CommandNotFoundError
9
12
  from sharkit.output.theme import BLUE, BOLD, GREEN, PINK, RED, RESET
10
13
  from sharkit.tools.base import Tool
11
14
 
@@ -22,6 +25,19 @@ def _normalize_bool(value: str) -> str:
22
25
  return value
23
26
 
24
27
 
28
+ def _make_progress_callback(
29
+ renderer: Any, tag: str, color: str, first_progress: list[bool]
30
+ ) -> Callable[[str], None]:
31
+ def on_progress(msg: str) -> None:
32
+ if renderer is not None:
33
+ if first_progress[0]:
34
+ renderer.gutter(tag, msg, color, is_first=True)
35
+ first_progress[0] = False
36
+ else:
37
+ renderer.gutter(tag, msg, color, is_first=False)
38
+ return on_progress
39
+
40
+
25
41
  def _get_tool_instance(context: CommandContext) -> Tool | None:
26
42
  tool_name = context.current_tool
27
43
  if tool_name is None:
@@ -70,7 +86,7 @@ class HelpCommand(Command):
70
86
  return "No command registry available."
71
87
  try:
72
88
  cmd_class = registry.get_command(name)
73
- except Exception:
89
+ except (CommandNotFoundError, KeyError):
74
90
  return f'Command "{name}" not found.'
75
91
  if cmd_class is None:
76
92
  return f'Command "{name}" not found.'
@@ -173,7 +189,7 @@ class UseCommand(Command):
173
189
  def _log(self, context: CommandContext, message: str) -> None:
174
190
  renderer = context.session.get("renderer")
175
191
  if renderer is not None:
176
- renderer.log_line("use", message)
192
+ renderer.dash_line("use", message)
177
193
 
178
194
  def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
179
195
  if not args:
@@ -194,15 +210,9 @@ class UseCommand(Command):
194
210
  return None
195
211
  renderer = context.session.get("renderer")
196
212
  first_progress = [True]
197
-
198
- def on_progress(msg: str) -> None:
199
- if renderer is not None:
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)
213
+ on_progress = _make_progress_callback(
214
+ renderer, f"install {display}", GREEN, first_progress
215
+ )
206
216
 
207
217
  ok, message = tool_manager.install(name, spec, on_progress=on_progress)
208
218
  if not ok:
@@ -268,7 +278,7 @@ class InfoCommand(Command):
268
278
  f"safety {metadata.safety}",
269
279
  ]
270
280
  return "\n".join(lines)
271
- except Exception:
281
+ except (CommandNotFoundError, AttributeError):
272
282
  return f"Tool: {context.current_tool}"
273
283
 
274
284
 
@@ -276,20 +286,22 @@ class ShowCommand(Command):
276
286
  name = "show"
277
287
  aliases = []
278
288
  description = "Show tools or tool options"
279
- usage = "show <tools|options> [tool]"
289
+ usage = "show <all|osint|humint|options> [tool]"
280
290
 
281
291
  def execute(self, context: CommandContext, args: list[str]) -> str | None:
282
292
  if not args:
283
293
  if context.current_tool is not None:
284
294
  return self._show_options(context, None)
285
- return self._show_tools(context)
295
+ return self._show_tools(context, None)
286
296
  if args[0] == "options":
287
297
  return self._show_options(context, args[1] if len(args) > 1 else None)
288
- if args[0] == "tools":
289
- return self._show_tools(context)
298
+ if args[0] == "all":
299
+ return self._show_tools(context, None)
300
+ if args[0] in ("osint", "humint"):
301
+ return self._show_tools(context, args[0])
290
302
  return f"Unknown show subcommand: {args[0]}\nUsage: {self.usage}"
291
303
 
292
- def _show_tools(self, context: CommandContext) -> str | None:
304
+ def _show_tools(self, context: CommandContext, category: str | None) -> str | None:
293
305
  tool_registry = context.session.get("tool_registry")
294
306
  tool_manager = context.session.get("tool_manager")
295
307
  renderer = context.session.get("renderer")
@@ -298,17 +310,32 @@ class ShowCommand(Command):
298
310
  all_tools = tool_registry.get_all_tools()
299
311
  if not all_tools:
300
312
  return "No tools available."
301
- rows = []
313
+
314
+ categories: dict[str, list[tuple[str, type]]] = {}
302
315
  for tool_path, tool_cls in all_tools.items():
303
- metadata = tool_cls.metadata
304
- if metadata.install is None or (
305
- tool_manager is not None and tool_manager.is_installed(metadata.name)
306
- ):
307
- installed = f"{BOLD}{PINK}Yes{RESET}"
308
- else:
309
- installed = ""
310
- rows.append([tool_path, installed, metadata.description])
311
- renderer.table("show > tools", ["Tool", "Installed", "Description"], rows)
316
+ meta = tool_cls.metadata
317
+ cat = meta.category.split(".")[0] if meta.category else "other"
318
+ if category and cat != category:
319
+ continue
320
+ categories.setdefault(cat, []).append((tool_path, tool_cls))
321
+
322
+ if not categories:
323
+ return f'No tools found for category "{category}".'
324
+
325
+ title = f"show > {category or 'all'}"
326
+ for cat_name in sorted(categories):
327
+ tools = sorted(categories[cat_name], key=lambda x: x[0])
328
+ rows = []
329
+ for tool_path, tool_cls in tools:
330
+ meta = tool_cls.metadata
331
+ if meta.install is None or (
332
+ tool_manager is not None and tool_manager.is_installed(meta.name)
333
+ ):
334
+ installed = f"{BOLD}{PINK}Yes{RESET}"
335
+ else:
336
+ installed = ""
337
+ rows.append([tool_path, installed, meta.description])
338
+ renderer.table(f"{title} > {cat_name}", ["Tool", "Installed", "Description"], rows)
312
339
  return None
313
340
 
314
341
  def _show_options(self, context: CommandContext, tool_arg: str | None) -> str | None:
@@ -319,12 +346,12 @@ class ShowCommand(Command):
319
346
  if tool_arg is not None:
320
347
  tool_cls = tool_registry.get_tool(tool_arg)
321
348
  if tool_cls is None:
322
- return f'Tool "{tool_arg}" not found.\nTry: show tools'
349
+ return f'Tool "{tool_arg}" not found.\nTry: show all'
323
350
  instance = tool_cls()
324
351
  title_tool = tool_cls.metadata.name
325
352
  else:
326
353
  if context.current_tool is None:
327
- return "No tool selected. Use 'show tools' or 'use <tool>' first."
354
+ return "No tool selected. Use 'show all' or 'use <tool>' first."
328
355
  tool_cls = tool_registry.get_tool(context.current_tool)
329
356
  if tool_cls is None:
330
357
  return "Current tool not found."
@@ -338,7 +365,11 @@ class ShowCommand(Command):
338
365
  return "This tool has no options."
339
366
  rows = []
340
367
  for opt in options.values():
341
- value = opt.value or opt.default or "not set"
368
+ value = (
369
+ opt.value
370
+ if opt.value is not None
371
+ else (opt.default if opt.default is not None else "not set")
372
+ )
342
373
  required = " (required)" if opt.required else ""
343
374
  rows.append([opt.name, value, f"{opt.description}{required}"])
344
375
  renderer.table(
@@ -347,7 +378,7 @@ class ShowCommand(Command):
347
378
  rows,
348
379
  )
349
380
  return None
350
- except Exception:
381
+ except (CommandNotFoundError, KeyError, AttributeError):
351
382
  return "Could not retrieve options for tool."
352
383
 
353
384
 
@@ -374,6 +405,8 @@ class SetCommand(Command):
374
405
  return f'Option "{key}" not found.\nAvailable options: {available}'
375
406
  option = options[key]
376
407
  raw = " ".join(args[1:])
408
+ if not raw and option.required:
409
+ return f'Option "{key}" requires a value.'
377
410
  if option.type == "bool":
378
411
  value = _normalize_bool(raw)
379
412
  elif option.choices:
@@ -394,7 +427,7 @@ class SetCommand(Command):
394
427
  display = tool_registry.format_display(
395
428
  tool_registry.get_tool_path(context.current_tool),
396
429
  )
397
- renderer.log_line(f"set {display}", f"{key} = {value}")
430
+ renderer.dash_line(f"set {display}", f"{key} = {value}")
398
431
  return None
399
432
  return f"Set {key} = {value}"
400
433
  except Exception as e:
@@ -452,7 +485,10 @@ class RunCommand(Command):
452
485
  return "Tool registry not available."
453
486
  try:
454
487
  options = tool_instance.get_options()
455
- option_values = {k: v.value or v.default or "" for k, v in options.items()}
488
+ option_values = {
489
+ k: v.value if v.value is not None else (v.default if v.default is not None else "")
490
+ for k, v in options.items()
491
+ }
456
492
  config_dir = context.session.get("config_dir") or Path("/tmp")
457
493
  from sharkit.tools.base import ExecutionContext
458
494
 
@@ -475,7 +511,9 @@ class RunCommand(Command):
475
511
  if result.success:
476
512
  if result.data:
477
513
  if renderer is not None:
478
- renderer.result(result.data)
514
+ tool_name = tool_instance.metadata.name
515
+ tool_color = tool_instance.metadata.color or "#888888"
516
+ renderer.tool_output(result.data, tool_name, tool_color)
479
517
  return None
480
518
  lines = [f"{key}: {value}" for key, value in result.data.items()]
481
519
  return "\n".join(lines) if lines else "Tool executed successfully."
@@ -486,7 +524,8 @@ class RunCommand(Command):
486
524
  renderer.error(f"Tool execution failed: {result.error}")
487
525
  return None
488
526
  return f"Tool execution failed: {result.error}"
489
- except Exception as e:
527
+ except (RuntimeError, ValueError, OSError) as e:
528
+ logging.getLogger(__name__).exception("Tool execution error")
490
529
  return f"Tool execution error: {e}"
491
530
 
492
531
 
@@ -500,7 +539,7 @@ class SearchCommand(Command):
500
539
  valid, error = self.validate_args(args)
501
540
  if not valid:
502
541
  return error
503
- query = args[0].lower()
542
+ query = " ".join(args).lower()
504
543
  tool_registry = context.session.get("tool_registry")
505
544
  if tool_registry is None:
506
545
  return "Tool registry not available."
@@ -648,15 +687,9 @@ class InstallCommand(Command):
648
687
  return None
649
688
  renderer = context.session.get("renderer")
650
689
  first_progress = [True]
651
-
652
- def on_progress(msg: str) -> None:
653
- if renderer is not None:
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)
690
+ on_progress = _make_progress_callback(
691
+ renderer, f"install {display}", GREEN, first_progress
692
+ )
660
693
 
661
694
  install_spec = tool_cls.metadata.install
662
695
  ok, message = tool_manager.install(
@@ -675,9 +708,8 @@ class InstallCommand(Command):
675
708
  renderer.log_line(tag, msg, color=GREEN)
676
709
  else:
677
710
  lines = msg.splitlines() or [""]
678
- renderer.gutter(tag, lines[0], GREEN, is_first=False)
679
- for line in lines[1:]:
680
- print(f"{GREEN}{BOLD}│{RESET} {line}")
711
+ for line in lines:
712
+ renderer.gutter(tag, line, GREEN, is_first=False)
681
713
  return None
682
714
  return str(message)
683
715
 
@@ -707,21 +739,12 @@ class UpgradeCommand(Command):
707
739
  tool_name = tool_cls.metadata.name
708
740
  display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
709
741
  if not tool_manager.is_installed(tool_name):
710
- renderer = context.session.get("renderer")
711
- if renderer is not None:
712
- renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
713
742
  return None
714
743
  renderer = context.session.get("renderer")
715
744
  first_progress = [True]
716
-
717
- def on_progress(msg: str) -> None:
718
- if renderer is not None:
719
- tag = f"upgrade {display}"
720
- if first_progress[0]:
721
- renderer.gutter(tag, msg, BLUE, is_first=True)
722
- first_progress[0] = False
723
- else:
724
- renderer.gutter(tag, msg, BLUE, is_first=False)
745
+ on_progress = _make_progress_callback(
746
+ renderer, f"upgrade {display}", BLUE, first_progress
747
+ )
725
748
 
726
749
  upgrade_spec = tool_cls.metadata.install
727
750
  ok, message = tool_manager.update(
@@ -740,9 +763,8 @@ class UpgradeCommand(Command):
740
763
  renderer.log_line(tag, msg, color=BLUE)
741
764
  else:
742
765
  lines = msg.splitlines() or [""]
743
- renderer.gutter(tag, lines[0], BLUE, is_first=False)
744
- for line in lines[1:]:
745
- print(f"{BLUE}{BOLD}│{RESET} {line}")
766
+ for line in lines:
767
+ renderer.gutter(tag, line, BLUE, is_first=False)
746
768
  return None
747
769
  return str(message)
748
770
 
@@ -770,25 +792,12 @@ class UpgradeCommand(Command):
770
792
  name = meta.name
771
793
  display = tool_registry.format_display(tool_registry.get_tool_path(name))
772
794
  if not tool_manager.is_installed(name):
773
- renderer = context.session.get("renderer")
774
- if renderer is not None:
775
- renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
776
795
  continue
777
796
  renderer = context.session.get("renderer")
778
797
  first_progress = [True]
779
-
780
- def on_progress(
781
- msg: str,
782
- _first: list[bool] = first_progress,
783
- _tag: str = f"upgrade {display}",
784
- _renderer: Any = renderer,
785
- ) -> None:
786
- if _renderer is not None:
787
- if _first[0]:
788
- _renderer.gutter(_tag, msg, BLUE, is_first=True)
789
- _first[0] = False
790
- else:
791
- _renderer.gutter(_tag, msg, BLUE, is_first=False)
798
+ on_progress = _make_progress_callback(
799
+ renderer, f"upgrade {display}", BLUE, first_progress
800
+ )
792
801
 
793
802
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
794
803
  if renderer is not None:
@@ -804,9 +813,8 @@ class UpgradeCommand(Command):
804
813
  renderer.log_line(tag, msg, color=BLUE)
805
814
  else:
806
815
  lines = msg.splitlines() or [""]
807
- renderer.gutter(tag, lines[0], BLUE, is_first=False)
808
- for line in lines[1:]:
809
- print(f"{BLUE}{BOLD}│{RESET} {line}")
816
+ for line in lines:
817
+ renderer.gutter(tag, line, BLUE, is_first=False)
810
818
  return None
811
819
 
812
820
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  from pathlib import Path
4
5
 
5
6
  from sharkit.config.paths import (
@@ -34,7 +35,7 @@ class ConfigManager:
34
35
  def _load(self) -> None:
35
36
  try:
36
37
  content = self._config_file.read_text(encoding="utf-8")
37
- except OSError as exc:
38
+ except (OSError, UnicodeDecodeError) as exc:
38
39
  raise ConfigurationError(f"Failed to read config file: {exc}") from exc
39
40
 
40
41
  for line_number, raw_line in enumerate(content.splitlines(), start=1):
@@ -49,16 +50,13 @@ class ConfigManager:
49
50
  self._config[key.strip()] = value.strip()
50
51
 
51
52
  def _save(self) -> None:
52
- lines = []
53
- for key, value in sorted(self._config.items()):
54
- lines.append(f"{key}={value}")
55
- content = "\n".join(lines)
56
- if content:
57
- content += "\n"
58
53
  try:
59
- self._config_file.write_text(content, encoding="utf-8")
54
+ tmp = self._config_file.with_suffix(".tmp")
55
+ lines = [f"{k}={v}" for k, v in sorted(self._config.items())]
56
+ tmp.write_text("\n".join(lines) + "\n" if lines else "", encoding="utf-8")
57
+ os.replace(tmp, self._config_file)
60
58
  except OSError as exc:
61
- raise ConfigurationError(f"Failed to write config file: {exc}") from exc
59
+ raise ConfigurationError(f"Failed to save config: {exc}") from exc
62
60
 
63
61
  def get(self, key: str, default: str | None = None) -> str | None:
64
62
  return self._config.get(key, default)
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
 
5
+ from sharkit.exceptions import ConfigurationError
6
+
5
7
  _BASE_DIR_NAME = "sharkit"
6
8
 
7
9
 
@@ -11,7 +13,12 @@ def _get_xdg_config_home() -> Path:
11
13
  xdg = os.environ.get("XDG_CONFIG_HOME")
12
14
  if xdg:
13
15
  return Path(xdg)
14
- return Path.home() / ".config"
16
+ try:
17
+ return Path.home() / ".config"
18
+ except RuntimeError:
19
+ raise ConfigurationError(
20
+ "Cannot determine home directory. Set XDG_CONFIG_HOME manually."
21
+ ) from None
15
22
 
16
23
 
17
24
  def get_config_dir() -> Path:
@@ -1,10 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sharkit.commands.registry import CommandRegistry
4
+ from sharkit.tools.base import OptionDefinition
4
5
  from sharkit.tools.registry import ToolRegistry
5
6
 
6
7
 
7
8
  class CompletionEngine:
9
+ def __init__(self) -> None:
10
+ self._cached_tool: str | None = None
11
+ self._cached_options: dict[str, OptionDefinition] | None = None
12
+
13
+ def _load_options(
14
+ self, tool_registry: ToolRegistry, current_tool: str
15
+ ) -> dict[str, OptionDefinition]:
16
+ if current_tool == self._cached_tool and self._cached_options is not None:
17
+ return self._cached_options
18
+ tool_cls = tool_registry.get_tool(current_tool)
19
+ if tool_cls is None:
20
+ self._cached_tool = None
21
+ self._cached_options = None
22
+ return {}
23
+ instance = tool_cls()
24
+ self._cached_options = instance.get_options()
25
+ self._cached_tool = current_tool
26
+ return self._cached_options
27
+
8
28
  def get_completions(
9
29
  self,
10
30
  text: str,
@@ -22,22 +42,36 @@ class CompletionEngine:
22
42
 
23
43
  if text_lower.startswith("show "):
24
44
  prefix = text_lower[5:]
25
- return sorted(s for s in ("options", "tools") if s.startswith(prefix))
45
+ return sorted(s for s in ("all", "osint", "humint", "options") if s.startswith(prefix))
26
46
 
27
47
  if text_lower.startswith("set ") or text_lower.startswith("unset "):
28
48
  if current_tool is None:
29
49
  return []
50
+ is_set = text_lower.startswith("set ")
51
+ body = text[4:] if is_set else text[6:]
52
+ parts = body.split(None, 1)
53
+
30
54
  try:
31
- tool_cls = tool_registry.get_tool(current_tool)
32
- if tool_cls is None:
33
- return []
34
- instance = tool_cls()
35
- options = instance.get_options()
36
- prefix = text_lower[4:] if text_lower.startswith("set ") else text_lower[6:]
37
- return sorted(k for k in options if k.lower().startswith(prefix))
55
+ options = self._load_options(tool_registry, current_tool)
38
56
  except Exception:
39
57
  return []
40
58
 
59
+ if len(parts) < 2:
60
+ prefix = parts[0] if parts else ""
61
+ exact = options.get(prefix)
62
+ if exact and exact.choices:
63
+ return list(exact.choices)
64
+ elif exact and exact.type == "bool":
65
+ return ["true", "false"]
66
+ return sorted(k for k in options if k.lower().startswith(prefix))
67
+
68
+ option_name = parts[0]
69
+ value_prefix = parts[1]
70
+ opt = options.get(option_name)
71
+ if opt is None or not opt.choices:
72
+ return []
73
+ return sorted(c for c in opt.choices if c.lower().startswith(value_prefix.lower()))
74
+
41
75
  if text_lower.startswith("install "):
42
76
  prefix = text_lower[8:]
43
77
  return sorted(
@@ -16,7 +16,19 @@ class PromptRenderer:
16
16
  formatted = tool_name
17
17
  return f"shark {formatted} > "
18
18
 
19
- def render(self, prompt: str) -> list[tuple[str, str]]:
19
+ def render(self, prompt: str, tool_name: str | None = None) -> list[tuple[str, str]]:
20
+ if tool_name is not None and "/" in tool_name:
21
+ cat, rest = tool_name.split("/", 1)
22
+ before = "shark "
23
+ after = " > "
24
+ return [
25
+ (PROMPT_STYLE, before),
26
+ (MODULE_STYLE, cat),
27
+ ("", "("),
28
+ (MODULE_STYLE, rest),
29
+ ("", ")"),
30
+ ("", after),
31
+ ]
20
32
  if "(" in prompt and ")" in prompt:
21
33
  before, rest = prompt.split("(", 1)
22
34
  tool_path, after = rest.split(")", 1)
@@ -25,6 +37,6 @@ class PromptRenderer:
25
37
  ("", "("),
26
38
  (MODULE_STYLE, tool_path),
27
39
  ("", ")"),
28
- ("", after),
40
+ (PROMPT_STYLE, after),
29
41
  ]
30
42
  return [(PROMPT_STYLE, prompt)]
@@ -31,7 +31,7 @@ class _SharkitHistory(History):
31
31
  super().__init__()
32
32
 
33
33
  def load_history_strings(self) -> list[str]:
34
- return self._manager.get_history()
34
+ return list(reversed(self._manager.get_history()))
35
35
 
36
36
  def store_string(self, string: str) -> None:
37
37
  self._manager.add_command(string)
@@ -108,7 +108,7 @@ class Console:
108
108
  prompt = self._prompt_renderer.tool_prompt(self._current_tool)
109
109
  else:
110
110
  prompt = self._prompt_renderer.default_prompt()
111
- return self._prompt_renderer.render(prompt)
111
+ return self._prompt_renderer.render(prompt, tool_name=self._current_tool)
112
112
 
113
113
  def _init_session(self) -> None:
114
114
  self._prompt_renderer = PromptRenderer()
@@ -144,8 +144,6 @@ class Console:
144
144
  if not stripped:
145
145
  continue
146
146
 
147
- if not line[:1].isspace():
148
- self._history_manager.add_command(stripped)
149
147
  self._session_data["history"] = self._history_manager.get_history()
150
148
 
151
149
  if not self.process_command(stripped):
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+
1
4
  class SharkitError(Exception):
2
5
  def __init__(self, message: str) -> None:
3
6
  super().__init__(message)