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.
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/PKG-INFO +2 -2
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/README.md +1 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/pyproject.toml +5 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/__init__.py +1 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/__main__.py +2 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/app.py +4 -2
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/base.py +3 -3
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/builtins.py +91 -69
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/manager.py +7 -9
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/paths.py +8 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/completion.py +45 -8
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/prompt.py +14 -2
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/repl.py +2 -4
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/exceptions.py +3 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/history/manager.py +20 -5
- sharkit-1.0.0/src/sharkit/network/client.py +95 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/output/renderer.py +33 -12
- sharkit-1.0.0/src/sharkit/output/theme.py +38 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/base.py +17 -5
- sharkit-1.0.0/src/sharkit/tools/flatten_tools.sh +107 -0
- sharkit-1.0.0/src/sharkit/tools/geoint/recon/wifi/mylnikov.py +79 -0
- sharkit-1.0.0/src/sharkit/tools/humint/__init__.py +0 -0
- sharkit-1.0.0/src/sharkit/tools/humint/factcheck/truthcheck.py +195 -0
- sharkit-1.0.0/src/sharkit/tools/humint/factcheck/verifact.py +188 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/loader.py +5 -1
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/manager.py +22 -8
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/crypto/mempool.py +85 -0
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/hashes/hash_decrypt.py +90 -0
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/hashes/hash_lookup.py +71 -0
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/images/exif_reader.py +160 -0
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/text/base64_tool.py +79 -0
- sharkit-1.0.0/src/sharkit/tools/osint/analytics/text/text_entropy.py +81 -0
- sharkit-1.0.0/src/sharkit/tools/osint/gather/scraperx.py +187 -0
- sharkit-1.0.0/src/sharkit/tools/osint/gather/scweet.py +194 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/darkweb/ahmia_search.py +74 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/darkweb/pyahmia.py +187 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/cve_search.py +155 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/dns_lookup.py +181 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/otx_query.py +126 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/urlhaus_check.py +127 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/domains/whois_lookup.py +141 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/email_reputation.py +87 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/email_security.py +131 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/holehe.py +210 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/mailaccess.py +199 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/emails/theharvester.py +216 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/osint/recon/hybrid/blackbird.py +12 -9
- sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/circl_hashlookup.py +77 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/hibp_passwords.py +83 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/leak_check.py +85 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/psbdmp.py +75 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/leaks/xposed_or_not.py +80 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/bgpv6.py +94 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/http_headers.py +108 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ip_geolocate.py +86 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ip_info.py +76 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/port_scanner.py +140 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/robtex.py +89 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/shodan_internetdb.py +80 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/network/ssl_check.py +134 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/phones/phoneinfoga.py +210 -0
- {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
- {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
- sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/social_analyzer.py +210 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/socialscan.py +198 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/usernames/socid_extractor.py +210 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/web/robots_check.py +138 -0
- sharkit-1.0.0/src/sharkit/tools/osint/recon/web/wayback_check.py +122 -0
- sharkit-0.2.1.dev5/src/sharkit/network/client.py +0 -81
- sharkit-0.2.1.dev5/src/sharkit/output/theme.py +0 -18
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/.gitignore +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/LICENSE +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/commands/registry.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/config/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/console/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/history/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/network/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/output/__init__.py +0 -0
- {sharkit-0.2.1.dev5 → sharkit-1.0.0}/src/sharkit/tools/__init__.py +0 -0
- {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.
|
|
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/
|
|
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/
|
|
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.
|
|
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"]
|
|
@@ -33,8 +33,9 @@ NON_LINUX_MESSAGE = (
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def main() -> int:
|
|
36
|
-
sys.stdout.
|
|
37
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
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 <
|
|
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] == "
|
|
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
|
|
303
|
-
|
|
304
|
-
if
|
|
305
|
-
tool_manager is not None and tool_manager.is_installed(
|
|
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,
|
|
311
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
653
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
779
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
(
|
|
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):
|