sharkit 0.2.1.dev5__tar.gz → 1.0.0__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 (81) hide show
  1. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/PKG-INFO +2 -2
  2. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/README.md +1 -1
  3. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/pyproject.toml +5 -1
  4. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/__init__.py +1 -1
  5. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/__main__.py +2 -0
  6. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/app.py +4 -2
  7. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/base.py +3 -3
  8. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/builtins.py +91 -69
  9. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/manager.py +7 -9
  10. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/paths.py +8 -1
  11. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/completion.py +45 -8
  12. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/prompt.py +14 -2
  13. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/repl.py +2 -4
  14. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/exceptions.py +3 -0
  15. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/history/manager.py +20 -5
  16. sharkit-1.0.0/src/sharkit/network/client.py +95 -0
  17. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/output/renderer.py +33 -12
  18. sharkit-1.0.0/src/sharkit/output/theme.py +38 -0
  19. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/base.py +17 -5
  20. sharkit-1.0.0/src/sharkit/tools/flatten_tools.sh +107 -0
  21. sharkit-1.0.0/src/sharkit/tools/geoint/recon/wifi/mylnikov.py +79 -0
  22. sharkit-1.0.0/src/sharkit/tools/humint/__init__.py +0 -0
  23. sharkit-1.0.0/src/sharkit/tools/humint/factcheck/truthcheck.py +195 -0
  24. sharkit-1.0.0/src/sharkit/tools/humint/factcheck/verifact.py +188 -0
  25. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/loader.py +5 -1
  26. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/manager.py +22 -8
  27. sharkit-1.0.0/src/sharkit/tools/osint/analytics/crypto/mempool.py +85 -0
  28. sharkit-1.0.0/src/sharkit/tools/osint/analytics/hashes/hash_decrypt.py +90 -0
  29. sharkit-1.0.0/src/sharkit/tools/osint/analytics/hashes/hash_lookup.py +71 -0
  30. sharkit-1.0.0/src/sharkit/tools/osint/analytics/images/exif_reader.py +160 -0
  31. sharkit-1.0.0/src/sharkit/tools/osint/analytics/text/base64_tool.py +79 -0
  32. sharkit-1.0.0/src/sharkit/tools/osint/analytics/text/text_entropy.py +81 -0
  33. sharkit-1.0.0/src/sharkit/tools/osint/gather/scraperx.py +187 -0
  34. sharkit-1.0.0/src/sharkit/tools/osint/gather/scweet.py +194 -0
  35. sharkit-1.0.0/src/sharkit/tools/osint/recon/darkweb/ahmia_search.py +74 -0
  36. sharkit-1.0.0/src/sharkit/tools/osint/recon/darkweb/pyahmia.py +187 -0
  37. sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/cve_search.py +155 -0
  38. sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/dns_lookup.py +181 -0
  39. sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/otx_query.py +126 -0
  40. sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/urlhaus_check.py +127 -0
  41. sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/whois_lookup.py +141 -0
  42. sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/email_reputation.py +87 -0
  43. sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/email_security.py +131 -0
  44. sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/holehe.py +210 -0
  45. sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/mailaccess.py +199 -0
  46. sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/theharvester.py +216 -0
  47. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/osint/recon/hybrid/blackbird.py +12 -9
  48. sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/circl_hashlookup.py +77 -0
  49. sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/hibp_passwords.py +83 -0
  50. sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/leak_check.py +85 -0
  51. sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/psbdmp.py +75 -0
  52. sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/xposed_or_not.py +80 -0
  53. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/bgpv6.py +94 -0
  54. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/http_headers.py +108 -0
  55. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ip_geolocate.py +86 -0
  56. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ip_info.py +76 -0
  57. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/port_scanner.py +140 -0
  58. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/robtex.py +89 -0
  59. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/shodan_internetdb.py +80 -0
  60. sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ssl_check.py +134 -0
  61. sharkit-1.0.0/src/sharkit/tools/osint/recon/phones/phoneinfoga.py +210 -0
  62. {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames}/maigret.py +12 -9
  63. {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames}/sherlock.py +12 -9
  64. sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/social_analyzer.py +210 -0
  65. sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/socialscan.py +198 -0
  66. sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/socid_extractor.py +210 -0
  67. sharkit-1.0.0/src/sharkit/tools/osint/recon/web/robots_check.py +138 -0
  68. sharkit-1.0.0/src/sharkit/tools/osint/recon/web/wayback_check.py +122 -0
  69. sharkit-0.2.1.dev5/src/sharkit/network/client.py +0 -81
  70. sharkit-0.2.1.dev5/src/sharkit/output/theme.py +0 -18
  71. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/.gitignore +0 -0
  72. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/LICENSE +0 -0
  73. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/__init__.py +0 -0
  74. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/registry.py +0 -0
  75. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/__init__.py +0 -0
  76. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/__init__.py +0 -0
  77. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/history/__init__.py +0 -0
  78. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/network/__init__.py +0 -0
  79. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/output/__init__.py +0 -0
  80. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/__init__.py +0 -0
  81. {sharkit-0.2.1.dev5 → sharkit-1.0.0}/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.dev5
3
+ Version: 1.0.0
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
@@ -26,7 +26,7 @@ Requires-Dist: httpx<1.0.0,>=0.27.0
26
26
  Requires-Dist: prompt-toolkit<4.0.0,>=3.0.0
27
27
  Description-Content-Type: text/markdown
28
28
 
29
- <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
29
+ <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/6bff669d-78f7-430f-88f7-49fefa832128" /></div>
30
30
  <br>
31
31
  <div align="center">
32
32
 
@@ -1,4 +1,4 @@
1
- <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
1
+ <div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/6bff669d-78f7-430f-88f7-49fefa832128" /></div>
2
2
  <br>
3
3
  <div align="center">
4
4
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sharkit"
3
- version = "0.2.1.dev5"
3
+ version = "1.0.0"
4
4
  description = "OSINT / HUMINT / GEOINT research framework"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -89,6 +89,10 @@ no_implicit_optional = true
89
89
  warn_redundant_casts = true
90
90
  warn_unused_ignores = true
91
91
 
92
+ [[tool.mypy.overrides]]
93
+ module = "exifread"
94
+ ignore_missing_imports = true
95
+
92
96
  [tool.pytest.ini_options]
93
97
  testpaths = ["tests"]
94
98
  pythonpath = ["src"]
@@ -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,39 +286,67 @@ 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|geoint|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", "geoint"):
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")
296
308
  if tool_registry is None or renderer is None:
297
309
  return "Tool registry not available."
310
+
298
311
  all_tools = tool_registry.get_all_tools()
299
312
  if not all_tools:
300
313
  return "No tools available."
314
+
315
+ def matches_category(tool_path: str, tool_cls: type[Tool]) -> bool:
316
+ if category is None:
317
+ return True
318
+
319
+ normalized_category = category.lower()
320
+ tool_category = (tool_cls.metadata.category or "").lower()
321
+ first_segment = tool_category.split(".")[0]
322
+ return (
323
+ normalized_category in (tool_category, first_segment)
324
+ or normalized_category in tool_category
325
+ or tool_path.lower().startswith(f"{normalized_category}/")
326
+ )
327
+
328
+ filtered = [
329
+ (tool_path, tool_cls)
330
+ for tool_path, tool_cls in all_tools.items()
331
+ if matches_category(tool_path, tool_cls)
332
+ ]
333
+
334
+ if not filtered:
335
+ return f'No tools found for category "{category}".'
336
+
337
+ title = f"show > {category or 'all'}"
301
338
  rows = []
302
- 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)
339
+ for tool_path, tool_cls in sorted(filtered, key=lambda item: item[0]):
340
+ meta = tool_cls.metadata
341
+ if meta.install is None or (
342
+ tool_manager is not None and tool_manager.is_installed(meta.name)
306
343
  ):
307
344
  installed = f"{BOLD}{PINK}Yes{RESET}"
308
345
  else:
309
346
  installed = ""
310
- rows.append([tool_path, installed, metadata.description])
311
- renderer.table("show > tools", ["Tool", "Installed", "Description"], rows)
347
+ rows.append([tool_path, installed, meta.description])
348
+
349
+ renderer.table(title, ["Tool", "Installed", "Description"], rows)
312
350
  return None
313
351
 
314
352
  def _show_options(self, context: CommandContext, tool_arg: str | None) -> str | None:
@@ -319,12 +357,12 @@ class ShowCommand(Command):
319
357
  if tool_arg is not None:
320
358
  tool_cls = tool_registry.get_tool(tool_arg)
321
359
  if tool_cls is None:
322
- return f'Tool "{tool_arg}" not found.\nTry: show tools'
360
+ return f'Tool "{tool_arg}" not found.\nTry: show all'
323
361
  instance = tool_cls()
324
362
  title_tool = tool_cls.metadata.name
325
363
  else:
326
364
  if context.current_tool is None:
327
- return "No tool selected. Use 'show tools' or 'use <tool>' first."
365
+ return "No tool selected. Use 'show all' or 'use <tool>' first."
328
366
  tool_cls = tool_registry.get_tool(context.current_tool)
329
367
  if tool_cls is None:
330
368
  return "Current tool not found."
@@ -338,7 +376,11 @@ class ShowCommand(Command):
338
376
  return "This tool has no options."
339
377
  rows = []
340
378
  for opt in options.values():
341
- value = opt.value or opt.default or "not set"
379
+ value = (
380
+ opt.value
381
+ if opt.value is not None
382
+ else (opt.default if opt.default is not None else "not set")
383
+ )
342
384
  required = " (required)" if opt.required else ""
343
385
  rows.append([opt.name, value, f"{opt.description}{required}"])
344
386
  renderer.table(
@@ -347,7 +389,7 @@ class ShowCommand(Command):
347
389
  rows,
348
390
  )
349
391
  return None
350
- except Exception:
392
+ except (CommandNotFoundError, KeyError, AttributeError):
351
393
  return "Could not retrieve options for tool."
352
394
 
353
395
 
@@ -374,6 +416,8 @@ class SetCommand(Command):
374
416
  return f'Option "{key}" not found.\nAvailable options: {available}'
375
417
  option = options[key]
376
418
  raw = " ".join(args[1:])
419
+ if not raw and option.required:
420
+ return f'Option "{key}" requires a value.'
377
421
  if option.type == "bool":
378
422
  value = _normalize_bool(raw)
379
423
  elif option.choices:
@@ -394,7 +438,7 @@ class SetCommand(Command):
394
438
  display = tool_registry.format_display(
395
439
  tool_registry.get_tool_path(context.current_tool),
396
440
  )
397
- renderer.log_line(f"set {display}", f"{key} = {value}")
441
+ renderer.dash_line(f"set {display}", f"{key} = {value}")
398
442
  return None
399
443
  return f"Set {key} = {value}"
400
444
  except Exception as e:
@@ -452,7 +496,10 @@ class RunCommand(Command):
452
496
  return "Tool registry not available."
453
497
  try:
454
498
  options = tool_instance.get_options()
455
- option_values = {k: v.value or v.default or "" for k, v in options.items()}
499
+ option_values = {
500
+ k: v.value if v.value is not None else (v.default if v.default is not None else "")
501
+ for k, v in options.items()
502
+ }
456
503
  config_dir = context.session.get("config_dir") or Path("/tmp")
457
504
  from sharkit.tools.base import ExecutionContext
458
505
 
@@ -475,7 +522,9 @@ class RunCommand(Command):
475
522
  if result.success:
476
523
  if result.data:
477
524
  if renderer is not None:
478
- renderer.result(result.data)
525
+ tool_name = tool_instance.metadata.name
526
+ tool_color = tool_instance.metadata.color or "#888888"
527
+ renderer.tool_output(result.data, tool_name, tool_color)
479
528
  return None
480
529
  lines = [f"{key}: {value}" for key, value in result.data.items()]
481
530
  return "\n".join(lines) if lines else "Tool executed successfully."
@@ -486,7 +535,8 @@ class RunCommand(Command):
486
535
  renderer.error(f"Tool execution failed: {result.error}")
487
536
  return None
488
537
  return f"Tool execution failed: {result.error}"
489
- except Exception as e:
538
+ except (RuntimeError, ValueError, OSError) as e:
539
+ logging.getLogger(__name__).exception("Tool execution error")
490
540
  return f"Tool execution error: {e}"
491
541
 
492
542
 
@@ -500,7 +550,7 @@ class SearchCommand(Command):
500
550
  valid, error = self.validate_args(args)
501
551
  if not valid:
502
552
  return error
503
- query = args[0].lower()
553
+ query = " ".join(args).lower()
504
554
  tool_registry = context.session.get("tool_registry")
505
555
  if tool_registry is None:
506
556
  return "Tool registry not available."
@@ -648,15 +698,9 @@ class InstallCommand(Command):
648
698
  return None
649
699
  renderer = context.session.get("renderer")
650
700
  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)
701
+ on_progress = _make_progress_callback(
702
+ renderer, f"install {display}", GREEN, first_progress
703
+ )
660
704
 
661
705
  install_spec = tool_cls.metadata.install
662
706
  ok, message = tool_manager.install(
@@ -706,21 +750,12 @@ class UpgradeCommand(Command):
706
750
  tool_name = tool_cls.metadata.name
707
751
  display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
708
752
  if not tool_manager.is_installed(tool_name):
709
- renderer = context.session.get("renderer")
710
- if renderer is not None:
711
- renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
712
753
  return None
713
754
  renderer = context.session.get("renderer")
714
755
  first_progress = [True]
715
-
716
- def on_progress(msg: str) -> None:
717
- if renderer is not None:
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)
756
+ on_progress = _make_progress_callback(
757
+ renderer, f"upgrade {display}", BLUE, first_progress
758
+ )
724
759
 
725
760
  upgrade_spec = tool_cls.metadata.install
726
761
  ok, message = tool_manager.update(
@@ -768,25 +803,12 @@ class UpgradeCommand(Command):
768
803
  name = meta.name
769
804
  display = tool_registry.format_display(tool_registry.get_tool_path(name))
770
805
  if not tool_manager.is_installed(name):
771
- renderer = context.session.get("renderer")
772
- if renderer is not None:
773
- renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
774
806
  continue
775
807
  renderer = context.session.get("renderer")
776
808
  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)
809
+ on_progress = _make_progress_callback(
810
+ renderer, f"upgrade {display}", BLUE, first_progress
811
+ )
790
812
 
791
813
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
792
814
  if renderer is not None:
@@ -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,39 @@ 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(
46
+ s for s in ("all", "osint", "humint", "geoint", "options")
47
+ if s.startswith(prefix)
48
+ )
26
49
 
27
50
  if text_lower.startswith("set ") or text_lower.startswith("unset "):
28
51
  if current_tool is None:
29
52
  return []
53
+ is_set = text_lower.startswith("set ")
54
+ body = text[4:] if is_set else text[6:]
55
+ parts = body.split(None, 1)
56
+
30
57
  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))
58
+ options = self._load_options(tool_registry, current_tool)
38
59
  except Exception:
39
60
  return []
40
61
 
62
+ if len(parts) < 2:
63
+ prefix = parts[0] if parts else ""
64
+ exact = options.get(prefix)
65
+ if exact and exact.choices:
66
+ return list(exact.choices)
67
+ elif exact and exact.type == "bool":
68
+ return ["true", "false"]
69
+ return sorted(k for k in options if k.lower().startswith(prefix))
70
+
71
+ option_name = parts[0]
72
+ value_prefix = parts[1]
73
+ opt = options.get(option_name)
74
+ if opt is None or not opt.choices:
75
+ return []
76
+ return sorted(c for c in opt.choices if c.lower().startswith(value_prefix.lower()))
77
+
41
78
  if text_lower.startswith("install "):
42
79
  prefix = text_lower[8:]
43
80
  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)