gulp-cli 0.0.0__py3-none-any.whl

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.
gulp_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """gulp-cli package."""
gulp_cli/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from gulp_cli.cli import app
4
+
5
+
6
+ def main() -> None:
7
+ app()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
gulp_cli/cli.py ADDED
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from gulp_cli.commands.acl import app as acl_app
6
+ from gulp_cli.commands.auth import app as auth_app
7
+ from gulp_cli.commands.collab import app as collab_app
8
+ from gulp_cli.commands.context import app as context_app
9
+ from gulp_cli.commands.db import app as db_app
10
+ from gulp_cli.commands.enhance_map import app as enhance_map_app
11
+ from gulp_cli.commands.glyph import app as glyph_app
12
+ from gulp_cli.commands.ingest import app as ingest_app
13
+ from gulp_cli.commands.mapping import app as mapping_app
14
+ from gulp_cli.commands.operations import app as operation_app
15
+ from gulp_cli.commands.plugin import app as plugin_app
16
+ from gulp_cli.commands.query import app as query_app
17
+ from gulp_cli.commands.source import app as source_app
18
+ from gulp_cli.commands.stats import app as stats_app
19
+ from gulp_cli.commands.storage import app as storage_app
20
+ from gulp_cli.commands.user_group import app as user_group_app
21
+ from gulp_cli.commands.users import app as user_app
22
+ from gulp_cli.config import set_runtime_as_user, set_runtime_verbose
23
+ from gulp_cli.extensions import load_extensions
24
+
25
+ app = typer.Typer(
26
+ no_args_is_help=True,
27
+ help="Modern CLI for gULP, powered by gulp-sdk",
28
+ )
29
+
30
+
31
+ @app.callback()
32
+ def main(
33
+ as_user: str | None = typer.Option(
34
+ None,
35
+ "--as-user",
36
+ help="Use the saved session of a different already-logged-in user for this command only.",
37
+ ),
38
+ verbose: bool = typer.Option(
39
+ False,
40
+ "--verbose",
41
+ help="Print complete result JSON instead of summary (global override).",
42
+ ),
43
+ ) -> None:
44
+ set_runtime_as_user(as_user)
45
+ set_runtime_verbose(verbose)
46
+
47
+ app.add_typer(auth_app, name="auth")
48
+ app.add_typer(user_app, name="user")
49
+ app.add_typer(user_group_app, name="user-group")
50
+ app.add_typer(operation_app, name="operation")
51
+ app.add_typer(context_app, name="context")
52
+ app.add_typer(source_app, name="source")
53
+ app.add_typer(ingest_app, name="ingest")
54
+ app.add_typer(query_app, name="query")
55
+ app.add_typer(stats_app, name="stats")
56
+ app.add_typer(storage_app, name="storage")
57
+ app.add_typer(collab_app, name="collab")
58
+ app.add_typer(db_app, name="db")
59
+ app.add_typer(acl_app, name="acl")
60
+ app.add_typer(plugin_app, name="plugin")
61
+ app.add_typer(mapping_app, name="mapping")
62
+ app.add_typer(enhance_map_app, name="enhance-map")
63
+ app.add_typer(glyph_app, name="glyph")
64
+
65
+ load_extensions(
66
+ app,
67
+ command_groups={
68
+ "auth": auth_app,
69
+ "user": user_app,
70
+ "user-group": user_group_app,
71
+ "operation": operation_app,
72
+ "context": context_app,
73
+ "source": source_app,
74
+ "ingest": ingest_app,
75
+ "query": query_app,
76
+ "stats": stats_app,
77
+ "storage": storage_app,
78
+ "collab": collab_app,
79
+ "db": db_app,
80
+ "acl": acl_app,
81
+ "plugin": plugin_app,
82
+ "mapping": mapping_app,
83
+ "enhance-map": enhance_map_app,
84
+ "glyph": glyph_app,
85
+ },
86
+ )
gulp_cli/client.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import asynccontextmanager
4
+ from typing import AsyncIterator
5
+
6
+ from gulp_sdk.client import GulpClient
7
+
8
+ from gulp_cli.config import get_required_url_token
9
+
10
+
11
+ @asynccontextmanager
12
+ async def get_client() -> AsyncIterator[GulpClient]:
13
+ url, token = get_required_url_token()
14
+ async with GulpClient(url, token=token) as client:
15
+ yield client
gulp_cli/config.py ADDED
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from contextvars import ContextVar
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ CONFIG_DIR = Path.home() / ".config" / "gulp-cli"
9
+ CONFIG_PATH = CONFIG_DIR / "config.json"
10
+ _AS_USER_OVERRIDE: ContextVar[str | None] = ContextVar("gulp_cli_as_user_override", default=None)
11
+ _VERBOSE_OVERRIDE: ContextVar[bool] = ContextVar("gulp_cli_verbose_override", default=False)
12
+
13
+
14
+ class CLIConfigError(Exception):
15
+ """Raised when CLI config is missing or invalid."""
16
+
17
+
18
+ def _normalize_config(data: dict[str, Any]) -> dict[str, Any]:
19
+ sessions_raw = data.get("sessions")
20
+ sessions: dict[str, dict[str, Any]] = {}
21
+
22
+ if isinstance(sessions_raw, dict):
23
+ for username, session in sessions_raw.items():
24
+ if not isinstance(session, dict):
25
+ continue
26
+
27
+ name = str(username).strip()
28
+ url = str(session.get("url") or data.get("url") or "").strip()
29
+ token = str(session.get("token") or "").strip()
30
+ user_id = str(session.get("user_id") or name).strip()
31
+
32
+ if not name or not url or not token:
33
+ continue
34
+
35
+ sessions[name] = {
36
+ "url": url.rstrip("/"),
37
+ "token": token,
38
+ "user_id": user_id,
39
+ "expires_at": session.get("expires_at"),
40
+ }
41
+ else:
42
+ url = str(data.get("url") or "").strip()
43
+ token = str(data.get("token") or "").strip()
44
+ user_id = str(data.get("user_id") or "").strip()
45
+ if url and token:
46
+ username = user_id or "default"
47
+ sessions[username] = {
48
+ "url": url.rstrip("/"),
49
+ "token": token,
50
+ "user_id": user_id or username,
51
+ "expires_at": data.get("expires_at"),
52
+ }
53
+
54
+ active_user = str(data.get("active_user") or "").strip()
55
+ if active_user not in sessions:
56
+ active_user = next(iter(sessions), "")
57
+
58
+ return {
59
+ "active_user": active_user,
60
+ "sessions": sessions,
61
+ }
62
+
63
+
64
+ def load_config() -> dict[str, Any]:
65
+ if not CONFIG_PATH.exists():
66
+ return {"active_user": "", "sessions": {}}
67
+ try:
68
+ raw = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
69
+ except json.JSONDecodeError as exc:
70
+ raise CLIConfigError(f"Invalid config JSON in {CONFIG_PATH}: {exc}") from exc
71
+ if not isinstance(raw, dict):
72
+ raise CLIConfigError(f"Invalid config JSON in {CONFIG_PATH}: root object must be a JSON object")
73
+ return _normalize_config(raw)
74
+
75
+
76
+ def save_config(data: dict[str, Any]) -> None:
77
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
78
+ normalized = _normalize_config(data)
79
+ CONFIG_PATH.write_text(json.dumps(normalized, indent=2, sort_keys=True), encoding="utf-8")
80
+
81
+
82
+ def clear_config() -> None:
83
+ if CONFIG_PATH.exists():
84
+ CONFIG_PATH.unlink()
85
+
86
+
87
+ def set_runtime_as_user(user_name: str | None) -> None:
88
+ value = str(user_name or "").strip() or None
89
+ _AS_USER_OVERRIDE.set(value)
90
+
91
+
92
+ def get_runtime_as_user() -> str | None:
93
+ return _AS_USER_OVERRIDE.get()
94
+
95
+
96
+ def set_runtime_verbose(verbose: bool) -> None:
97
+ _VERBOSE_OVERRIDE.set(bool(verbose))
98
+
99
+
100
+ def get_runtime_verbose() -> bool:
101
+ return _VERBOSE_OVERRIDE.get()
102
+
103
+
104
+ def get_saved_users() -> list[str]:
105
+ return list(load_config().get("sessions", {}).keys())
106
+
107
+
108
+ def save_session(
109
+ *,
110
+ url: str,
111
+ username: str,
112
+ token: str,
113
+ user_id: str,
114
+ expires_at: Any,
115
+ make_active: bool = True,
116
+ ) -> dict[str, Any]:
117
+ cfg = load_config()
118
+ sessions = dict(cfg.get("sessions") or {})
119
+ name = username.strip()
120
+ sessions[name] = {
121
+ "url": url.rstrip("/"),
122
+ "token": token,
123
+ "user_id": user_id.strip() or name,
124
+ "expires_at": expires_at,
125
+ }
126
+ cfg["sessions"] = sessions
127
+ if make_active or not cfg.get("active_user"):
128
+ cfg["active_user"] = name
129
+ save_config(cfg)
130
+ return cfg
131
+
132
+
133
+ def get_selected_session(user_name: str | None = None) -> dict[str, Any]:
134
+ cfg = load_config()
135
+ sessions = cfg.get("sessions") or {}
136
+ selected = (user_name or get_runtime_as_user() or cfg.get("active_user") or "").strip()
137
+
138
+ if not sessions:
139
+ raise CLIConfigError("Not authenticated. Run: gulp-cli auth login --url <url> --username <u> --password <p>")
140
+
141
+ if not selected:
142
+ selected = next(iter(sessions), "")
143
+
144
+ session = sessions.get(selected)
145
+ if not isinstance(session, dict):
146
+ available = ", ".join(sorted(sessions))
147
+ raise CLIConfigError(
148
+ f"User '{selected}' is not logged in. Logged-in users: {available}"
149
+ )
150
+
151
+ return {"username": selected, **session}
152
+
153
+
154
+ def delete_session(user_name: str | None = None) -> dict[str, Any] | None:
155
+ cfg = load_config()
156
+ sessions = dict(cfg.get("sessions") or {})
157
+ selected = (user_name or get_runtime_as_user() or cfg.get("active_user") or "").strip()
158
+ if not selected:
159
+ return None
160
+
161
+ removed = sessions.pop(selected, None)
162
+ if removed is None:
163
+ return None
164
+
165
+ if sessions:
166
+ if cfg.get("active_user") == selected:
167
+ cfg["active_user"] = next(iter(sessions))
168
+ cfg["sessions"] = sessions
169
+ save_config(cfg)
170
+ else:
171
+ clear_config()
172
+
173
+ return {"username": selected, **removed}
174
+
175
+
176
+ def get_required_url_token() -> tuple[str, str]:
177
+ session = get_selected_session()
178
+ url = str(session.get("url") or "").strip()
179
+ token = str(session.get("token") or "").strip()
180
+ if not url or not token:
181
+ raise CLIConfigError("Not authenticated. Run: gulp-cli auth login --url <url> --username <u> --password <p>")
182
+ return url, token
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from gulp_sdk.api.request_utils import wait_for_request_stats
6
+ from gulp_sdk.websocket import WSMessage
7
+
8
+
9
+ async def call_custom_endpoint(
10
+ client: Any,
11
+ *,
12
+ method: str,
13
+ path: str,
14
+ params: dict[str, Any] | None = None,
15
+ json_body: Any = None,
16
+ ensure_websocket: bool = False,
17
+ wait: bool = False,
18
+ timeout: int = 120,
19
+ ws_callback: Callable[[WSMessage], None] | None = None,
20
+ ) -> dict[str, Any]:
21
+ """
22
+ Execute a direct API call for extension endpoints.
23
+
24
+ Optionally ensures websocket connection and waits for terminal request stats
25
+ when server responds with JSend pending status.
26
+ """
27
+ if ensure_websocket or wait:
28
+ await client.ensure_websocket()
29
+
30
+ response: dict[str, Any] = await client._request(
31
+ method,
32
+ path,
33
+ params=params,
34
+ json=json_body,
35
+ )
36
+
37
+ if (
38
+ wait
39
+ and isinstance(response, dict)
40
+ and response.get("status") == "pending"
41
+ and response.get("req_id")
42
+ ):
43
+ return await wait_for_request_stats(
44
+ client,
45
+ str(response.get("req_id")),
46
+ timeout,
47
+ ws_callback=ws_callback,
48
+ )
49
+
50
+ return response
gulp_cli/extensions.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import inspect
5
+ from pathlib import Path
6
+ from typing import Any, Callable
7
+
8
+ import typer
9
+
10
+ from gulp_cli.client import get_client
11
+ from gulp_cli.config import CONFIG_DIR
12
+ from gulp_cli.output import print_warning
13
+
14
+ INTERNAL_EXTENSIONS_DIR = Path(__file__).resolve().parent / "extension"
15
+ EXTERNAL_EXTENSIONS_DIR = CONFIG_DIR / "extension"
16
+
17
+
18
+ def _is_valid_extension_file(path: Path) -> bool:
19
+ return path.is_file() and path.suffix == ".py" and not path.name.startswith("_")
20
+
21
+
22
+ def discover_extensions() -> dict[str, Path]:
23
+ """
24
+ Discover extension modules from internal and external extension folders.
25
+
26
+ External modules have priority when a filename exists in both folders.
27
+ """
28
+ discovered: dict[str, Path] = {}
29
+
30
+ if INTERNAL_EXTENSIONS_DIR.exists():
31
+ for path in sorted(INTERNAL_EXTENSIONS_DIR.iterdir()):
32
+ if _is_valid_extension_file(path):
33
+ discovered[path.name] = path
34
+
35
+ if EXTERNAL_EXTENSIONS_DIR.exists():
36
+ for path in sorted(EXTERNAL_EXTENSIONS_DIR.iterdir()):
37
+ if _is_valid_extension_file(path):
38
+ discovered[path.name] = path
39
+
40
+ return discovered
41
+
42
+
43
+ def _load_extension_module(module_name: str, path: Path) -> Any:
44
+ spec = importlib.util.spec_from_file_location(module_name, path)
45
+ if spec is None or spec.loader is None:
46
+ raise RuntimeError(f"Cannot load extension module spec from {path}")
47
+ module = importlib.util.module_from_spec(spec)
48
+ spec.loader.exec_module(module)
49
+ return module
50
+
51
+
52
+ def _call_register_extension(
53
+ register_fn: Callable[..., Any],
54
+ *,
55
+ app: typer.Typer,
56
+ command_groups: dict[str, typer.Typer],
57
+ ) -> None:
58
+ """
59
+ Call register_extension with a flexible, backward-compatible signature.
60
+ """
61
+ supplied_kwargs: dict[str, Any] = {
62
+ "app": app,
63
+ "root_app": app,
64
+ "command_groups": command_groups,
65
+ "get_client": get_client,
66
+ }
67
+
68
+ signature = inspect.signature(register_fn)
69
+ accepts_var_kwargs = any(
70
+ p.kind == inspect.Parameter.VAR_KEYWORD
71
+ for p in signature.parameters.values()
72
+ )
73
+
74
+ kwargs: dict[str, Any] = {}
75
+ for name in signature.parameters:
76
+ if name in supplied_kwargs:
77
+ kwargs[name] = supplied_kwargs[name]
78
+
79
+ if accepts_var_kwargs:
80
+ kwargs = dict(supplied_kwargs)
81
+
82
+ if kwargs:
83
+ register_fn(**kwargs)
84
+ return
85
+
86
+ if signature.parameters:
87
+ register_fn(app)
88
+ return
89
+
90
+ register_fn()
91
+
92
+
93
+ def load_extensions(
94
+ app: typer.Typer,
95
+ *,
96
+ command_groups: dict[str, typer.Typer] | None = None,
97
+ ) -> list[str]:
98
+ """
99
+ Load and register CLI extensions.
100
+
101
+ Returns:
102
+ List of loaded extension filenames.
103
+ """
104
+ loaded: list[str] = []
105
+ groups = command_groups or {}
106
+
107
+ for filename, path in sorted(discover_extensions().items()):
108
+ module_name = f"gulp_cli_extension_{path.stem}"
109
+ try:
110
+ module = _load_extension_module(module_name, path)
111
+ except Exception as exc:
112
+ print_warning(f"Failed to load extension {filename}: {exc}")
113
+ continue
114
+
115
+ register_fn = getattr(module, "register_extension", None)
116
+ if register_fn is None:
117
+ print_warning(
118
+ f"Skipping extension {filename}: missing register_extension function"
119
+ )
120
+ continue
121
+
122
+ if not callable(register_fn):
123
+ print_warning(
124
+ f"Skipping extension {filename}: register_extension is not callable"
125
+ )
126
+ continue
127
+
128
+ try:
129
+ _call_register_extension(
130
+ register_fn,
131
+ app=app,
132
+ command_groups=groups,
133
+ )
134
+ except Exception as exc:
135
+ print_warning(f"Failed to register extension {filename}: {exc}")
136
+ continue
137
+
138
+ loaded.append(filename)
139
+
140
+ return loaded
gulp_cli/output.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from rich.console import Console
6
+ from rich.json import JSON
7
+ from rich.table import Table
8
+
9
+ from gulp_cli.config import get_runtime_verbose
10
+
11
+ console = Console()
12
+
13
+
14
+ def print_json(data: Any) -> None:
15
+ console.print(JSON.from_data(data))
16
+
17
+
18
+ def print_error(message: str) -> None:
19
+ console.print(f"[red]Error: {message}[/red]")
20
+
21
+
22
+ def print_warning(message: str) -> None:
23
+ console.print(f"[yellow]Warning: {message}[/yellow]")
24
+
25
+
26
+ def print_kv_table(rows: list[tuple[str, Any]], title: str | None = None) -> None:
27
+ table = Table(title=title)
28
+ table.add_column("Key", style="cyan")
29
+ table.add_column("Value", style="white")
30
+ for key, value in rows:
31
+ table.add_row(str(key), str(value))
32
+ console.print(table)
33
+
34
+
35
+ def print_records(records: list[dict[str, Any]], title: str | None = None) -> None:
36
+ if not records:
37
+ console.print("[yellow]No results[/yellow]")
38
+ return
39
+
40
+ keys: list[str] = []
41
+ for record in records:
42
+ for key in record.keys():
43
+ if key not in keys:
44
+ keys.append(key)
45
+
46
+ table = Table(title=title)
47
+ for key in keys:
48
+ table.add_column(str(key), overflow="fold")
49
+
50
+ for record in records:
51
+ table.add_row(*[str(record.get(key, "")) for key in keys])
52
+
53
+ console.print(table)
54
+
55
+
56
+ def print_result(
57
+ data: Any,
58
+ verbose: bool | None = None,
59
+ formatter: Callable[[Any], None] | None = None,
60
+ ) -> None:
61
+ """
62
+ Print result data with conditional verbose mode.
63
+
64
+ Args:
65
+ data: The result data to print
66
+ verbose: If True, always print full JSON output; if None, use global runtime verbose
67
+ formatter: Callable that formats/prints the summary (called only if verbose=False)
68
+ """
69
+ if verbose is None:
70
+ verbose = get_runtime_verbose()
71
+
72
+ if verbose:
73
+ print_json(data)
74
+ elif formatter:
75
+ formatter(data)
76
+ else:
77
+ # Default: print full JSON if no formatter provided
78
+ print_json(data)
gulp_cli/utils.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+
9
+ def parse_json_option(raw: str | None, *, field_name: str) -> dict[str, Any] | None:
10
+ if raw is None:
11
+ return None
12
+ text = raw.strip()
13
+ if not text:
14
+ return None
15
+ try:
16
+ data = json.loads(text)
17
+ except json.JSONDecodeError as exc:
18
+ raise typer.BadParameter(f"Invalid JSON for --{field_name}: {exc}") from exc
19
+ if not isinstance(data, dict):
20
+ raise typer.BadParameter(f"--{field_name} must be a JSON object")
21
+ return data
22
+
23
+
24
+ def parse_json_list_option(raw: str | None, *, field_name: str) -> list[dict[str, Any]] | None:
25
+ if raw is None:
26
+ return None
27
+ text = raw.strip()
28
+ if not text:
29
+ return None
30
+ try:
31
+ data = json.loads(text)
32
+ except json.JSONDecodeError as exc:
33
+ raise typer.BadParameter(f"Invalid JSON for --{field_name}: {exc}") from exc
34
+ if isinstance(data, dict):
35
+ return [data]
36
+ if isinstance(data, list) and all(isinstance(item, dict) for item in data):
37
+ return data
38
+ raise typer.BadParameter(f"--{field_name} must be a JSON object or a list of JSON objects")
39
+
40
+
41
+ def comma_split(raw: str | None) -> list[str]:
42
+ if not raw:
43
+ return []
44
+ return [item.strip() for item in raw.split(",") if item.strip()]
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: gulp-cli
3
+ Version: 0.0.0
4
+ Summary: Command-line client for gULP
5
+ Author-email: Mentat <info@mentat.is>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.12.5
9
+ Requires-Dist: rich>=13.7.1
10
+ Requires-Dist: click==8.1.7
11
+ Requires-Dist: gulp-sdk
12
+
13
+ # πŸš€ gulp-cli
14
+
15
+ > THIS IS STILL WIP! for the cli to work, set `"ws_ignore_missing": true` in your `gulp_cfg.json` to prevent the backend from halting operations when the CLI disconnects its websocket after sending an async request. This is a temporary workaround until (if)we implement a proper solution for maintaining the websocket lifecycle in the CLI.
16
+
17
+ **A modern, powerful command-line interface for gULP** β€” manage forensic document ingestion, querying, enrichment, and collaboration entirely from your terminal.
18
+
19
+ ## ✨ What can you do?
20
+
21
+ - πŸ” **Authentication** β€” secure login with token persistence
22
+ - πŸ“₯ **Ingestion** β€” ingest files (single/batch/wildcard), zip archives, with concurrent uploads
23
+ - πŸ” **Querying** β€” raw OpenSearch queries, Sigma rules, external plugins
24
+ - 🏷️ **Enrichment** β€” enrich documents, tag/untag, update fields
25
+ - πŸ‘₯ **User Management** β€” create users, manage permissions (admin only)
26
+ - πŸ“‹ **Operations** β€” create/list/manage operations and contexts
27
+ - πŸ”Œ **Plugins** β€” list/upload/download plugins and mapping files
28
+ - πŸ—ΊοΈ **Enhance Maps** β€” map `gulp.event_code` to glyph/color per plugin
29
+ - πŸ–ΌοΈ **Glyphs** β€” create/list/update/delete custom glyphs
30
+ - 🧩 **Dynamic Extensions** β€” load custom CLI commands from internal or user extension folders
31
+ - πŸ“Š **Stats** β€” monitor ingestion and query requests
32
+ - 🎯 **Collaboration** β€” manage notes, links, highlights
33
+
34
+ All with **beautiful terminal output**, **automatic tab completion**, and **async-first design**.
35
+
36
+ ---
37
+
38
+ ## πŸš€ Quick Start
39
+
40
+ ### Installation
41
+
42
+ ```bash
43
+ # Temporary setup: gulp-sdk is currently installed from GitHub via gulp-cli dependencies.
44
+ # In the future, both gulp-sdk and gulp-cli will be installable directly from PyPI.
45
+ # for now, clone this repository and install in development mode.
46
+ # you can use your existing gulp .venv or create a new one ...
47
+ python3 -m venv ./.venv
48
+ source ./.venv/bin/activate
49
+ git clone https://github.com/mentat-is/gulp-cli
50
+ cd gulp-cli && pip install -e .
51
+
52
+ # Verify installation
53
+ gulp-cli --help
54
+ ```
55
+
56
+ ### Basic Usage
57
+
58
+ ```bash
59
+ # Login to your gULP instance
60
+ gulp-cli auth login --url http://localhost:8080 --username admin --password admin
61
+
62
+ # Check who you are
63
+ gulp-cli auth whoami
64
+
65
+ # List operations
66
+ gulp-cli operation list
67
+
68
+ # Ingest files with wildcard
69
+ gulp-cli ingest file my_operation win_evtx 'samples/win_evtx/*.evtx'
70
+
71
+ # Query documents
72
+ gulp-cli query raw my_operation --q '{"query":{"match_all":{}}}'
73
+ ```
74
+ ---
75
+
76
+ ## πŸ“š Documentation
77
+
78
+ - **[Getting Started Guide](./docs/getting-started.md)** β€” auth, first operation, first ingest
79
+ - **[Command Reference](./docs/command-reference.md)** β€” all available commands and options
80
+ - **[Extensions Guide](./docs/extensions.md)** β€” dynamic extension loading and custom command contract
81
+ - **[Resource Management Commands](./docs/resource-management.md)** β€” context, source, plugin, mapping, enhance-map, glyph
82
+ - **[Practical Examples](./docs/examples.md)** β€” real-world workflows and recipes
83
+ - **[Troubleshooting](./docs/troubleshooting-cli.md)** β€” common issues and solutions
84
+
85
+ ---
@@ -0,0 +1,14 @@
1
+ gulp_cli/__init__.py,sha256=9eyhAp_FfXQO5PdWKgLM-WRLqaO294EP888aszYDwRQ,24
2
+ gulp_cli/__main__.py,sha256=cneTsFHHzojBDytw_a8L1dNnCxIFYW-JBxz8IuCYcuA,137
3
+ gulp_cli/cli.py,sha256=9HUXprfMaalDusiLnssKJS8b4Z37sLW6X1LctsoCotg,2920
4
+ gulp_cli/client.py,sha256=R_cXA94qOINW1Hfpepsq3ihgzjk9ibAAWl66tbIakmA,398
5
+ gulp_cli/config.py,sha256=pMRx_Mr-Hzg3egO3FckXZw7b0Ie6Nb89j6eNhfam4dc,5632
6
+ gulp_cli/extension_helpers.py,sha256=j7QxrSPP3JAZFuDi6ok37Ch3nwF9flSISqKkVxn4ckU,1260
7
+ gulp_cli/extensions.py,sha256=m-C0gJ6-_RG2NI-hajLIhXP80qXSUXOPDCs9vdbr5Vs,3923
8
+ gulp_cli/output.py,sha256=VIbDBcN9Ohnyi7PcV2DpQCPDBYqD1Ij-IeetD-Ss8ts,2033
9
+ gulp_cli/utils.py,sha256=d0tqpRW5V5t5qTHXejIqjBQS4rSF1I7ENeB9oKsjgqg,1328
10
+ gulp_cli-0.0.0.dist-info/METADATA,sha256=x6OBoBzQVSFbuMpnQelkCRmiyK4V2HDnTi7eEJ5wCLc,3410
11
+ gulp_cli-0.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ gulp_cli-0.0.0.dist-info/entry_points.txt,sha256=_1KOhvP8p49Nwm9KR383JAYVfCvUia2aKdArCJ43ORY,52
13
+ gulp_cli-0.0.0.dist-info/top_level.txt,sha256=0-7D6OZIho4wqM2BZdxk6xOAdAVwo6orXeZW-TqeMfk,9
14
+ gulp_cli-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gulp-cli = gulp_cli.__main__:main
@@ -0,0 +1 @@
1
+ gulp_cli