sharkit 0.2.1.dev5__tar.gz → 1.0.0.dev2__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.dev2}/PKG-INFO +1 -1
  2. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/pyproject.toml +1 -1
  3. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/__init__.py +1 -1
  4. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/__main__.py +2 -0
  5. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/app.py +4 -2
  6. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/base.py +3 -3
  7. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/builtins.py +92 -69
  8. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/manager.py +7 -9
  9. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/paths.py +8 -1
  10. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/completion.py +45 -8
  11. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/prompt.py +14 -2
  12. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/repl.py +2 -4
  13. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/exceptions.py +3 -0
  14. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/history/manager.py +20 -5
  15. sharkit-1.0.0.dev2/src/sharkit/network/client.py +95 -0
  16. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/output/renderer.py +33 -12
  17. sharkit-1.0.0.dev2/src/sharkit/output/theme.py +38 -0
  18. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/base.py +16 -4
  19. sharkit-1.0.0.dev2/src/sharkit/tools/flatten_tools.sh +107 -0
  20. sharkit-1.0.0.dev2/src/sharkit/tools/geoint/recon/wifi/mylnikov.py +79 -0
  21. sharkit-1.0.0.dev2/src/sharkit/tools/humint/__init__.py +0 -0
  22. sharkit-1.0.0.dev2/src/sharkit/tools/humint/factcheck/truthcheck.py +195 -0
  23. sharkit-1.0.0.dev2/src/sharkit/tools/humint/factcheck/verifact.py +188 -0
  24. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/loader.py +5 -1
  25. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/manager.py +22 -8
  26. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/crypto/mempool.py +85 -0
  27. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/hashes/hash_decrypt.py +90 -0
  28. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/hashes/hash_lookup.py +71 -0
  29. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/images/exif_reader.py +157 -0
  30. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/text/base64_tool.py +79 -0
  31. sharkit-1.0.0.dev2/src/sharkit/tools/osint/analytics/text/text_entropy.py +81 -0
  32. sharkit-1.0.0.dev2/src/sharkit/tools/osint/gather/scraperx.py +187 -0
  33. sharkit-1.0.0.dev2/src/sharkit/tools/osint/gather/scweet.py +194 -0
  34. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/darkweb/ahmia_search.py +74 -0
  35. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/darkweb/pyahmia.py +187 -0
  36. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/cve_search.py +155 -0
  37. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/dns_lookup.py +180 -0
  38. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/otx_query.py +126 -0
  39. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/urlhaus_check.py +127 -0
  40. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/domains/whois_lookup.py +141 -0
  41. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/email_reputation.py +87 -0
  42. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/email_security.py +131 -0
  43. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/holehe.py +210 -0
  44. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/mailaccess.py +199 -0
  45. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/emails/theharvester.py +216 -0
  46. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/osint/recon/hybrid/blackbird.py +12 -9
  47. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/circl_hashlookup.py +77 -0
  48. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/hibp_passwords.py +83 -0
  49. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/leak_check.py +85 -0
  50. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/psbdmp.py +75 -0
  51. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/leaks/xposed_or_not.py +80 -0
  52. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/bgpv6.py +94 -0
  53. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/http_headers.py +108 -0
  54. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ip_geolocate.py +86 -0
  55. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ip_info.py +76 -0
  56. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/port_scanner.py +140 -0
  57. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/robtex.py +89 -0
  58. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/shodan_internetdb.py +80 -0
  59. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/network/ssl_check.py +116 -0
  60. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/phones/phoneinfoga.py +210 -0
  61. {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames}/maigret.py +12 -9
  62. {sharkit-0.2.1.dev5/src/sharkit/tools/osint/recon/username → sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames}/sherlock.py +12 -9
  63. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/social_analyzer.py +210 -0
  64. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/socialscan.py +198 -0
  65. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/usernames/socid_extractor.py +210 -0
  66. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/web/robots_check.py +138 -0
  67. sharkit-1.0.0.dev2/src/sharkit/tools/osint/recon/web/wayback_check.py +122 -0
  68. sharkit-0.2.1.dev5/src/sharkit/network/client.py +0 -81
  69. sharkit-0.2.1.dev5/src/sharkit/output/theme.py +0 -18
  70. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/.gitignore +0 -0
  71. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/LICENSE +0 -0
  72. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/README.md +0 -0
  73. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/__init__.py +0 -0
  74. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/commands/registry.py +0 -0
  75. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/config/__init__.py +0 -0
  76. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/console/__init__.py +0 -0
  77. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/history/__init__.py +0 -0
  78. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/network/__init__.py +0 -0
  79. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/output/__init__.py +0 -0
  80. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/src/sharkit/tools/__init__.py +0 -0
  81. {sharkit-0.2.1.dev5 → sharkit-1.0.0.dev2}/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.dev2
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.dev5"
3
+ version = "1.0.0.dev2"
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,39 +286,68 @@ 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) -> 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 == tool_category
324
+ or normalized_category == first_segment
325
+ or normalized_category in tool_category
326
+ or tool_path.lower().startswith(f"{normalized_category}/")
327
+ )
328
+
329
+ filtered = [
330
+ (tool_path, tool_cls)
331
+ for tool_path, tool_cls in all_tools.items()
332
+ if matches_category(tool_path, tool_cls)
333
+ ]
334
+
335
+ if not filtered:
336
+ return f'No tools found for category "{category}".'
337
+
338
+ title = f"show > {category or 'all'}"
301
339
  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)
340
+ for tool_path, tool_cls in sorted(filtered, key=lambda item: item[0]):
341
+ meta = tool_cls.metadata
342
+ if meta.install is None or (
343
+ tool_manager is not None and tool_manager.is_installed(meta.name)
306
344
  ):
307
345
  installed = f"{BOLD}{PINK}Yes{RESET}"
308
346
  else:
309
347
  installed = ""
310
- rows.append([tool_path, installed, metadata.description])
311
- renderer.table("show > tools", ["Tool", "Installed", "Description"], rows)
348
+ rows.append([tool_path, installed, meta.description])
349
+
350
+ renderer.table(title, ["Tool", "Installed", "Description"], rows)
312
351
  return None
313
352
 
314
353
  def _show_options(self, context: CommandContext, tool_arg: str | None) -> str | None:
@@ -319,12 +358,12 @@ class ShowCommand(Command):
319
358
  if tool_arg is not None:
320
359
  tool_cls = tool_registry.get_tool(tool_arg)
321
360
  if tool_cls is None:
322
- return f'Tool "{tool_arg}" not found.\nTry: show tools'
361
+ return f'Tool "{tool_arg}" not found.\nTry: show all'
323
362
  instance = tool_cls()
324
363
  title_tool = tool_cls.metadata.name
325
364
  else:
326
365
  if context.current_tool is None:
327
- return "No tool selected. Use 'show tools' or 'use <tool>' first."
366
+ return "No tool selected. Use 'show all' or 'use <tool>' first."
328
367
  tool_cls = tool_registry.get_tool(context.current_tool)
329
368
  if tool_cls is None:
330
369
  return "Current tool not found."
@@ -338,7 +377,11 @@ class ShowCommand(Command):
338
377
  return "This tool has no options."
339
378
  rows = []
340
379
  for opt in options.values():
341
- value = opt.value or opt.default or "not set"
380
+ value = (
381
+ opt.value
382
+ if opt.value is not None
383
+ else (opt.default if opt.default is not None else "not set")
384
+ )
342
385
  required = " (required)" if opt.required else ""
343
386
  rows.append([opt.name, value, f"{opt.description}{required}"])
344
387
  renderer.table(
@@ -347,7 +390,7 @@ class ShowCommand(Command):
347
390
  rows,
348
391
  )
349
392
  return None
350
- except Exception:
393
+ except (CommandNotFoundError, KeyError, AttributeError):
351
394
  return "Could not retrieve options for tool."
352
395
 
353
396
 
@@ -374,6 +417,8 @@ class SetCommand(Command):
374
417
  return f'Option "{key}" not found.\nAvailable options: {available}'
375
418
  option = options[key]
376
419
  raw = " ".join(args[1:])
420
+ if not raw and option.required:
421
+ return f'Option "{key}" requires a value.'
377
422
  if option.type == "bool":
378
423
  value = _normalize_bool(raw)
379
424
  elif option.choices:
@@ -394,7 +439,7 @@ class SetCommand(Command):
394
439
  display = tool_registry.format_display(
395
440
  tool_registry.get_tool_path(context.current_tool),
396
441
  )
397
- renderer.log_line(f"set {display}", f"{key} = {value}")
442
+ renderer.dash_line(f"set {display}", f"{key} = {value}")
398
443
  return None
399
444
  return f"Set {key} = {value}"
400
445
  except Exception as e:
@@ -452,7 +497,10 @@ class RunCommand(Command):
452
497
  return "Tool registry not available."
453
498
  try:
454
499
  options = tool_instance.get_options()
455
- option_values = {k: v.value or v.default or "" for k, v in options.items()}
500
+ option_values = {
501
+ k: v.value if v.value is not None else (v.default if v.default is not None else "")
502
+ for k, v in options.items()
503
+ }
456
504
  config_dir = context.session.get("config_dir") or Path("/tmp")
457
505
  from sharkit.tools.base import ExecutionContext
458
506
 
@@ -475,7 +523,9 @@ class RunCommand(Command):
475
523
  if result.success:
476
524
  if result.data:
477
525
  if renderer is not None:
478
- renderer.result(result.data)
526
+ tool_name = tool_instance.metadata.name
527
+ tool_color = tool_instance.metadata.color or "#888888"
528
+ renderer.tool_output(result.data, tool_name, tool_color)
479
529
  return None
480
530
  lines = [f"{key}: {value}" for key, value in result.data.items()]
481
531
  return "\n".join(lines) if lines else "Tool executed successfully."
@@ -486,7 +536,8 @@ class RunCommand(Command):
486
536
  renderer.error(f"Tool execution failed: {result.error}")
487
537
  return None
488
538
  return f"Tool execution failed: {result.error}"
489
- except Exception as e:
539
+ except (RuntimeError, ValueError, OSError) as e:
540
+ logging.getLogger(__name__).exception("Tool execution error")
490
541
  return f"Tool execution error: {e}"
491
542
 
492
543
 
@@ -500,7 +551,7 @@ class SearchCommand(Command):
500
551
  valid, error = self.validate_args(args)
501
552
  if not valid:
502
553
  return error
503
- query = args[0].lower()
554
+ query = " ".join(args).lower()
504
555
  tool_registry = context.session.get("tool_registry")
505
556
  if tool_registry is None:
506
557
  return "Tool registry not available."
@@ -648,15 +699,9 @@ class InstallCommand(Command):
648
699
  return None
649
700
  renderer = context.session.get("renderer")
650
701
  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)
702
+ on_progress = _make_progress_callback(
703
+ renderer, f"install {display}", GREEN, first_progress
704
+ )
660
705
 
661
706
  install_spec = tool_cls.metadata.install
662
707
  ok, message = tool_manager.install(
@@ -706,21 +751,12 @@ class UpgradeCommand(Command):
706
751
  tool_name = tool_cls.metadata.name
707
752
  display = tool_registry.format_display(tool_registry.get_tool_path(tool_name))
708
753
  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
754
  return None
713
755
  renderer = context.session.get("renderer")
714
756
  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)
757
+ on_progress = _make_progress_callback(
758
+ renderer, f"upgrade {display}", BLUE, first_progress
759
+ )
724
760
 
725
761
  upgrade_spec = tool_cls.metadata.install
726
762
  ok, message = tool_manager.update(
@@ -768,25 +804,12 @@ class UpgradeCommand(Command):
768
804
  name = meta.name
769
805
  display = tool_registry.format_display(tool_registry.get_tool_path(name))
770
806
  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
807
  continue
775
808
  renderer = context.session.get("renderer")
776
809
  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)
810
+ on_progress = _make_progress_callback(
811
+ renderer, f"upgrade {display}", BLUE, first_progress
812
+ )
790
813
 
791
814
  ok, message = tool_manager.update(name, spec, on_progress=on_progress)
792
815
  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)
@@ -6,11 +6,13 @@ from pathlib import Path
6
6
  from sharkit.config.paths import get_history_file
7
7
  from sharkit.exceptions import ConfigurationError
8
8
 
9
- _SENSITIVE_PATTERNS = re.compile(
10
- r"(password|passwd|token|api[_-]?key|secret|auth|credential)",
9
+ _SENSITIVE_RE = re.compile(
10
+ r"\b(password|passwd|token|api[_-]?key|secret|auth_token|credential)\b",
11
11
  re.IGNORECASE,
12
12
  )
13
13
 
14
+ MAX_HISTORY = 10_000
15
+
14
16
 
15
17
  class HistoryManager:
16
18
  _history_file: Path
@@ -46,7 +48,7 @@ class HistoryManager:
46
48
  raise ConfigurationError(f"Failed to write history file: {exc}") from exc
47
49
 
48
50
  def _is_sensitive(self, command: str) -> bool:
49
- return bool(_SENSITIVE_PATTERNS.search(command))
51
+ return bool(_SENSITIVE_RE.search(command))
50
52
 
51
53
  def add_command(self, command: str) -> None:
52
54
  if command[:1].isspace():
@@ -60,6 +62,8 @@ class HistoryManager:
60
62
  if lines and lines[-1] == stripped:
61
63
  return
62
64
  lines.append(stripped)
65
+ if len(lines) > MAX_HISTORY:
66
+ lines = lines[-MAX_HISTORY:]
63
67
  self._write_lines(lines)
64
68
 
65
69
  def get_history(self, limit: int = 100) -> list[str]:
@@ -74,10 +78,21 @@ class HistoryManager:
74
78
  commands.append(line)
75
79
  return commands[-limit:]
76
80
 
77
- def search(self, query: str) -> list[str]:
81
+ def search(self, query: str, limit: int = 50) -> list[str]:
78
82
  lines = self._read_lines()
79
83
  query_lower = query.lower()
80
- return [line for line in lines if query_lower in line.lower()]
84
+ results = []
85
+ for line in reversed(lines):
86
+ if not line:
87
+ continue
88
+ if line.startswith("#"):
89
+ continue
90
+ display = line[1:] if line.startswith("+") else line
91
+ if query_lower in display.lower():
92
+ results.append(display)
93
+ if len(results) >= limit:
94
+ break
95
+ return results
81
96
 
82
97
  def clear(self) -> None:
83
98
  self._write_lines([])