janito 2.28.0__py3-none-any.whl → 2.30.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.
- janito/README.md +3 -3
- janito/agent/setup_agent.py +21 -35
- janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +6 -0
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +6 -0
- janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +7 -1
- janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +7 -1
- janito/cli/chat_mode/session.py +154 -96
- janito/cli/cli_commands/list_plugins.py +99 -75
- janito/cli/cli_commands/show_system_prompt.py +8 -3
- janito/cli/core/runner.py +2 -2
- janito/cli/main_cli.py +9 -15
- janito/cli/prompt_core.py +0 -2
- janito/cli/rich_terminal_reporter.py +2 -1
- janito/cli/single_shot_mode/handler.py +0 -2
- janito/llm/agent.py +6 -1
- janito/provider_registry.py +1 -1
- janito/providers/openai/provider.py +1 -1
- janito/tools/adapters/local/ask_user.py +3 -1
- janito/tools/adapters/local/fetch_url.py +20 -28
- janito/tools/adapters/local/replace_text_in_file.py +9 -3
- janito/tools/adapters/local/search_text/core.py +2 -2
- janito/tools/loop_protection_decorator.py +12 -16
- janito/tools/tools_adapter.py +18 -4
- janito-2.30.0.dist-info/METADATA +83 -0
- {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/RECORD +29 -30
- janito-2.30.0.dist-info/licenses/LICENSE +201 -0
- janito/cli/chat_mode/session_profile_select.py +0 -182
- janito-2.28.0.dist-info/METADATA +0 -431
- janito-2.28.0.dist-info/licenses/LICENSE +0 -21
- {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/WHEEL +0 -0
- {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/entry_points.txt +0 -0
- {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/top_level.txt +0 -0
@@ -35,6 +35,7 @@ def _prepare_context(args, agent_role, allowed_permissions):
|
|
35
35
|
context["role"] = agent_role or "developer"
|
36
36
|
context["profile"] = getattr(args, "profile", None)
|
37
37
|
context["allowed_permissions"] = allowed_permissions
|
38
|
+
context["emoji_enabled"] = getattr(args, "emoji", False)
|
38
39
|
if allowed_permissions and "x" in allowed_permissions:
|
39
40
|
pd = PlatformDiscovery()
|
40
41
|
context["platform"] = pd.get_platform_name()
|
@@ -122,6 +123,10 @@ def handle_show_system_prompt(args):
|
|
122
123
|
if profile is None and getattr(args, "market", False):
|
123
124
|
profile = "Market Analyst"
|
124
125
|
|
126
|
+
# Handle --developer flag mapping to Developer With Python Tools profile
|
127
|
+
if profile is None and getattr(args, "developer", False):
|
128
|
+
profile = "Developer With Python Tools"
|
129
|
+
|
125
130
|
if not profile:
|
126
131
|
print(
|
127
132
|
"[janito] No profile specified. The main agent runs without a system prompt template.\n"
|
@@ -152,9 +157,9 @@ def handle_show_system_prompt(args):
|
|
152
157
|
system_prompt = template.render(**context)
|
153
158
|
system_prompt = re.sub(r"\n{3,}", "\n\n", system_prompt)
|
154
159
|
|
155
|
-
|
156
|
-
|
157
|
-
)
|
160
|
+
# Use the actual profile name for display, not the resolved value
|
161
|
+
display_profile = profile or "main"
|
162
|
+
print(f"\n--- System Prompt (resolved, profile: {display_profile}) ---\n")
|
158
163
|
print(system_prompt)
|
159
164
|
print("\n-------------------------------\n")
|
160
165
|
if agent_role:
|
janito/cli/core/runner.py
CHANGED
@@ -109,8 +109,8 @@ def prepare_llm_driver_config(args, modifiers):
|
|
109
109
|
llm_driver_config = LLMDriverConfig(**driver_config_data)
|
110
110
|
if getattr(llm_driver_config, "verbose_api", None):
|
111
111
|
pass
|
112
|
-
|
113
|
-
agent_role = modifiers.get("
|
112
|
+
|
113
|
+
agent_role = modifiers.get("profile") or "developer"
|
114
114
|
return provider, llm_driver_config, agent_role
|
115
115
|
|
116
116
|
|
janito/cli/main_cli.py
CHANGED
@@ -51,14 +51,6 @@ definition = [
|
|
51
51
|
"help": "Start with the Market Analyst profile (equivalent to --profile 'Market Analyst')",
|
52
52
|
},
|
53
53
|
),
|
54
|
-
(
|
55
|
-
["--role"],
|
56
|
-
{
|
57
|
-
"metavar": "ROLE",
|
58
|
-
"help": "Select the developer role name (overrides profile, e.g. 'python-expert').",
|
59
|
-
"default": None,
|
60
|
-
},
|
61
|
-
),
|
62
54
|
(
|
63
55
|
["-W", "--workdir"],
|
64
56
|
{
|
@@ -205,6 +197,13 @@ definition = [
|
|
205
197
|
"help": "Set the reasoning effort for models that support it (low, medium, high, none)",
|
206
198
|
},
|
207
199
|
),
|
200
|
+
(
|
201
|
+
["--emoji"],
|
202
|
+
{
|
203
|
+
"action": "store_true",
|
204
|
+
"help": "Enable emoji usage in responses to make output more engaging and expressive",
|
205
|
+
},
|
206
|
+
),
|
208
207
|
(["user_prompt"], {"nargs": argparse.REMAINDER, "help": "Prompt to submit"}),
|
209
208
|
(
|
210
209
|
["-e", "--event-log"],
|
@@ -244,7 +243,6 @@ definition = [
|
|
244
243
|
MODIFIER_KEYS = [
|
245
244
|
"provider",
|
246
245
|
"model",
|
247
|
-
"role",
|
248
246
|
"profile",
|
249
247
|
"developer",
|
250
248
|
"market",
|
@@ -257,6 +255,7 @@ MODIFIER_KEYS = [
|
|
257
255
|
"exec",
|
258
256
|
"read",
|
259
257
|
"write",
|
258
|
+
"emoji",
|
260
259
|
]
|
261
260
|
SETTER_KEYS = ["set", "set_provider", "set_api_key", "unset"]
|
262
261
|
GETTER_KEYS = [
|
@@ -372,9 +371,7 @@ class JanitoCLI:
|
|
372
371
|
for k in MODIFIER_KEYS
|
373
372
|
if getattr(self.args, k, None) is not None
|
374
373
|
}
|
375
|
-
|
376
|
-
if getattr(self.args, "role", None):
|
377
|
-
modifiers["role"] = getattr(self.args, "role")
|
374
|
+
|
378
375
|
return modifiers
|
379
376
|
|
380
377
|
def classify(self):
|
@@ -420,9 +417,6 @@ class JanitoCLI:
|
|
420
417
|
self.args.exec = True
|
421
418
|
# Remove the /rwx prefix from the prompt
|
422
419
|
self.args.user_prompt = self.args.user_prompt[1:]
|
423
|
-
elif self.args.user_prompt and self.args.user_prompt[0].startswith("/"):
|
424
|
-
# Skip LLM processing for other commands that start with /
|
425
|
-
return
|
426
420
|
|
427
421
|
# If running in single shot mode and --profile is not provided, default to 'developer' profile
|
428
422
|
# Skip profile selection for list commands that don't need it
|
janito/cli/prompt_core.py
CHANGED
@@ -216,8 +216,6 @@ class PromptHandler:
|
|
216
216
|
if on_event and final_event is not None:
|
217
217
|
on_event(final_event)
|
218
218
|
global_event_bus.publish(final_event)
|
219
|
-
# Terminal bell moved to token summary printing in session.py and handler.py
|
220
|
-
pass # print('\a', end='', flush=True)
|
221
219
|
except KeyboardInterrupt:
|
222
220
|
# Capture user interrupt / cancellation
|
223
221
|
self.console.print("[red]Interrupted by the user.[/red]")
|
@@ -139,11 +139,12 @@ class RichTerminalReporter(EventHandlerBase):
|
|
139
139
|
if not msg or not subtype:
|
140
140
|
return
|
141
141
|
if subtype == ReportSubtype.ACTION_INFO:
|
142
|
-
# Use orange for modification actions
|
142
|
+
# Use orange for all write/modification actions
|
143
143
|
modification_actions = (
|
144
144
|
getattr(ReportAction, "UPDATE", None),
|
145
145
|
getattr(ReportAction, "WRITE", None),
|
146
146
|
getattr(ReportAction, "DELETE", None),
|
147
|
+
getattr(ReportAction, "CREATE", None),
|
147
148
|
)
|
148
149
|
style = (
|
149
150
|
"orange1"
|
@@ -125,8 +125,6 @@ class PromptHandler:
|
|
125
125
|
print_token_message_summary(
|
126
126
|
shared_console, msg_count=1, usage=usage, elapsed=elapsed
|
127
127
|
)
|
128
|
-
# Send terminal bell character to trigger TUI bell after printing token summary
|
129
|
-
print("\a", end="", flush=True)
|
130
128
|
self._cleanup_driver_and_console()
|
131
129
|
|
132
130
|
def _cleanup_driver_and_console(self):
|
janito/llm/agent.py
CHANGED
@@ -244,7 +244,12 @@ class LLMAgent:
|
|
244
244
|
f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
|
245
245
|
)
|
246
246
|
tool_calls.append(part)
|
247
|
-
|
247
|
+
try:
|
248
|
+
result = self.tools_adapter.execute_function_call_message_part(part)
|
249
|
+
except Exception as e:
|
250
|
+
# Catch any exception during tool execution and return as string
|
251
|
+
# instead of letting it propagate to the user
|
252
|
+
result = str(e)
|
248
253
|
tool_results.append(result)
|
249
254
|
if tool_calls:
|
250
255
|
# Prepare tool_calls message for assistant
|
janito/provider_registry.py
CHANGED
@@ -41,7 +41,7 @@ class ProviderRegistry:
|
|
41
41
|
rows.append(info[:3])
|
42
42
|
|
43
43
|
# Group providers by openness (open-source first, then proprietary)
|
44
|
-
open_providers = {"cerebras", "deepseek", "alibaba", "
|
44
|
+
open_providers = {"cerebras", "deepseek", "alibaba", "moonshot", "zai"}
|
45
45
|
|
46
46
|
def sort_key(row):
|
47
47
|
provider_name = row[0]
|
@@ -18,7 +18,7 @@ class OpenAIProvider(LLMProvider):
|
|
18
18
|
MAINTAINER = "João Pinto <janito@ikignosis.org>"
|
19
19
|
MODEL_SPECS = MODEL_SPECS
|
20
20
|
DEFAULT_MODEL = (
|
21
|
-
"gpt-
|
21
|
+
"gpt-5" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
|
22
22
|
)
|
23
23
|
|
24
24
|
def __init__(
|
@@ -5,6 +5,7 @@ from janito.tools.loop_protection_decorator import protect_against_loops
|
|
5
5
|
from rich import print as rich_print
|
6
6
|
from janito.i18n import tr
|
7
7
|
from rich.panel import Panel
|
8
|
+
from rich.markdown import Markdown
|
8
9
|
from prompt_toolkit import PromptSession
|
9
10
|
from prompt_toolkit.key_binding import KeyBindings
|
10
11
|
from prompt_toolkit.enums import EditingMode
|
@@ -36,7 +37,7 @@ class AskUserTool(ToolBase):
|
|
36
37
|
def run(self, question: str) -> str:
|
37
38
|
|
38
39
|
print() # Print an empty line before the question panel
|
39
|
-
rich_print(Panel.fit(question, title=tr("Question"), style="cyan"))
|
40
|
+
rich_print(Panel.fit(Markdown(question), title=tr("Question"), style="cyan"))
|
40
41
|
|
41
42
|
bindings = KeyBindings()
|
42
43
|
mode = {"multiline": False}
|
@@ -107,4 +108,5 @@ class AskUserTool(ToolBase):
|
|
107
108
|
rich_print(
|
108
109
|
"[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
|
109
110
|
)
|
111
|
+
print("\a", end="", flush=True) # Print bell character
|
110
112
|
return sanitized
|
@@ -196,25 +196,15 @@ class FetchUrlTool(ToolBase):
|
|
196
196
|
whitelist_manager = get_url_whitelist_manager()
|
197
197
|
|
198
198
|
if not whitelist_manager.is_url_allowed(url):
|
199
|
-
error_message = tr(
|
200
|
-
"Warning: URL blocked by whitelist: {url}",
|
201
|
-
url=url,
|
202
|
-
)
|
199
|
+
error_message = tr("Blocked")
|
203
200
|
self.report_error(
|
204
|
-
tr(
|
205
|
-
"❗ URL blocked by whitelist: {url}",
|
206
|
-
url=url,
|
207
|
-
),
|
201
|
+
tr("❗ Blocked"),
|
208
202
|
ReportAction.READ,
|
209
203
|
)
|
210
204
|
return error_message
|
211
205
|
|
212
206
|
# Check session cache first
|
213
207
|
if url in self.session_cache:
|
214
|
-
self.report_warning(
|
215
|
-
tr("ℹ️ Using session cache"),
|
216
|
-
ReportAction.READ,
|
217
|
-
)
|
218
208
|
return self.session_cache[url]
|
219
209
|
|
220
210
|
# Check persistent cache for known errors
|
@@ -258,9 +248,8 @@ class FetchUrlTool(ToolBase):
|
|
258
248
|
status_code = http_err.response.status_code if http_err.response else None
|
259
249
|
if status_code and 400 <= status_code < 500:
|
260
250
|
error_message = tr(
|
261
|
-
"
|
251
|
+
"HTTP {status_code}",
|
262
252
|
status_code=status_code,
|
263
|
-
url=url,
|
264
253
|
)
|
265
254
|
# Cache 403 and 404 errors
|
266
255
|
if status_code in [403, 404]:
|
@@ -268,9 +257,8 @@ class FetchUrlTool(ToolBase):
|
|
268
257
|
|
269
258
|
self.report_error(
|
270
259
|
tr(
|
271
|
-
"❗ HTTP {status_code}
|
260
|
+
"❗ HTTP {status_code}",
|
272
261
|
status_code=status_code,
|
273
|
-
url=url,
|
274
262
|
),
|
275
263
|
ReportAction.READ,
|
276
264
|
)
|
@@ -278,25 +266,21 @@ class FetchUrlTool(ToolBase):
|
|
278
266
|
else:
|
279
267
|
self.report_error(
|
280
268
|
tr(
|
281
|
-
"❗ HTTP
|
282
|
-
|
283
|
-
err=str(http_err),
|
269
|
+
"❗ HTTP {status_code}",
|
270
|
+
status_code=status_code or "Error",
|
284
271
|
),
|
285
272
|
ReportAction.READ,
|
286
273
|
)
|
287
274
|
return tr(
|
288
|
-
"
|
289
|
-
|
290
|
-
err=str(http_err),
|
275
|
+
"HTTP {status_code}",
|
276
|
+
status_code=status_code or "Error",
|
291
277
|
)
|
292
278
|
except Exception as err:
|
293
279
|
self.report_error(
|
294
|
-
tr("❗ Error
|
280
|
+
tr("❗ Error"),
|
295
281
|
ReportAction.READ,
|
296
282
|
)
|
297
|
-
return tr(
|
298
|
-
"Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
|
299
|
-
)
|
283
|
+
return tr("Error")
|
300
284
|
|
301
285
|
def _extract_and_clean_text(self, html_content: str) -> str:
|
302
286
|
"""Extract and clean text from HTML content."""
|
@@ -370,7 +354,11 @@ class FetchUrlTool(ToolBase):
|
|
370
354
|
cookies=cookies,
|
371
355
|
follow_redirects=follow_redirects,
|
372
356
|
)
|
373
|
-
if
|
357
|
+
if (
|
358
|
+
html_content.startswith("HTTP ")
|
359
|
+
or html_content == "Error"
|
360
|
+
or html_content == "Blocked"
|
361
|
+
):
|
374
362
|
return html_content
|
375
363
|
|
376
364
|
try:
|
@@ -399,7 +387,11 @@ class FetchUrlTool(ToolBase):
|
|
399
387
|
cookies=cookies,
|
400
388
|
follow_redirects=follow_redirects,
|
401
389
|
)
|
402
|
-
if
|
390
|
+
if (
|
391
|
+
html_content.startswith("HTTP ")
|
392
|
+
or html_content == "Error"
|
393
|
+
or html_content == "Blocked"
|
394
|
+
):
|
403
395
|
return html_content
|
404
396
|
|
405
397
|
# Extract and clean text
|
@@ -159,16 +159,22 @@ class ReplaceTextInFileTool(ToolBase):
|
|
159
159
|
)
|
160
160
|
return warning, concise_warning
|
161
161
|
|
162
|
-
def _report_success(self, match_lines):
|
162
|
+
def _report_success(self, match_lines, line_delta_str=""):
|
163
163
|
"""Report success with line numbers where replacements occurred."""
|
164
164
|
if match_lines:
|
165
165
|
lines_str = ", ".join(str(line_no) for line_no in match_lines)
|
166
166
|
self.report_success(
|
167
|
-
tr(
|
167
|
+
tr(
|
168
|
+
" ✅ replaced at {lines_str}{delta}",
|
169
|
+
lines_str=lines_str,
|
170
|
+
delta=line_delta_str,
|
171
|
+
),
|
168
172
|
ReportAction.CREATE,
|
169
173
|
)
|
170
174
|
else:
|
171
|
-
self.report_success(
|
175
|
+
self.report_success(
|
176
|
+
tr(" ✅ replaced{delta}", delta=line_delta_str), ReportAction.CREATE
|
177
|
+
)
|
172
178
|
|
173
179
|
def _get_line_delta_str(self, content, new_content):
|
174
180
|
"""Return a string describing the net line change after replacement."""
|
@@ -98,7 +98,7 @@ class SearchTextTool(ToolBase):
|
|
98
98
|
if max_depth > 0:
|
99
99
|
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
100
100
|
if count_only:
|
101
|
-
info_str += " [count
|
101
|
+
info_str += " [count]"
|
102
102
|
self.report_action(info_str, ReportAction.READ)
|
103
103
|
if os.path.isfile(search_path):
|
104
104
|
dir_output, dir_limit_reached, per_file_counts = self._handle_file(
|
@@ -144,7 +144,7 @@ class SearchTextTool(ToolBase):
|
|
144
144
|
file_word_max = file_word + (" (max)" if dir_limit_reached else "")
|
145
145
|
self.report_success(
|
146
146
|
tr(
|
147
|
-
" ✅ {count} {file_word}
|
147
|
+
" ✅ {count} {file_word}/{num_files} {file_label}",
|
148
148
|
count=count,
|
149
149
|
file_word=file_word_max,
|
150
150
|
num_files=num_files,
|
@@ -119,22 +119,18 @@ def protect_against_loops(
|
|
119
119
|
current_time - timestamp <= time_window
|
120
120
|
for timestamp in _decorator_call_tracker[op_name]
|
121
121
|
):
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
raise RuntimeError(error_msg)
|
136
|
-
|
137
|
-
_report_error_and_raise(args, op_name)
|
122
|
+
# Return loop protection message as string instead of raising exception
|
123
|
+
error_msg = f"Loop protection: Too many {op_name} operations in a short time period ({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
|
124
|
+
|
125
|
+
# Try to report the error through the tool's reporting mechanism
|
126
|
+
tool_instance = args[0] if args else None
|
127
|
+
if hasattr(tool_instance, "report_error"):
|
128
|
+
try:
|
129
|
+
tool_instance.report_error(error_msg)
|
130
|
+
except Exception:
|
131
|
+
pass # If reporting fails, we still return the message
|
132
|
+
|
133
|
+
return error_msg
|
138
134
|
|
139
135
|
# Record this call
|
140
136
|
if op_name not in _decorator_call_tracker:
|
janito/tools/tools_adapter.py
CHANGED
@@ -460,9 +460,9 @@ class ToolsAdapterBase:
|
|
460
460
|
raise ToolCallException(tool_name, error_msg, arguments=arguments)
|
461
461
|
|
462
462
|
def _handle_execution_error(self, tool_name, request_id, exception, arguments):
|
463
|
-
# Check if this is a loop protection error that should
|
463
|
+
# Check if this is a loop protection error that should trigger a new strategy
|
464
464
|
if isinstance(exception, RuntimeError) and "Loop protection:" in str(exception):
|
465
|
-
error_msg = str(exception)
|
465
|
+
error_msg = str(exception)
|
466
466
|
if self._event_bus:
|
467
467
|
self._event_bus.publish(
|
468
468
|
ToolCallError(
|
@@ -473,8 +473,22 @@ class ToolsAdapterBase:
|
|
473
473
|
arguments=arguments,
|
474
474
|
)
|
475
475
|
)
|
476
|
-
# Return the
|
477
|
-
return error_msg
|
476
|
+
# Return the loop protection message as string to trigger new strategy
|
477
|
+
return f"Loop protection triggered - requesting new strategy: {error_msg}"
|
478
|
+
|
479
|
+
# Check if this is a string return from loop protection (new behavior)
|
480
|
+
if isinstance(exception, str) and "Loop protection:" in exception:
|
481
|
+
error_msg = str(exception)
|
482
|
+
if self._event_bus:
|
483
|
+
self._event_bus.publish(
|
484
|
+
ToolCallError(
|
485
|
+
tool_name=tool_name,
|
486
|
+
request_id=request_id,
|
487
|
+
error=error_msg,
|
488
|
+
arguments=arguments,
|
489
|
+
)
|
490
|
+
)
|
491
|
+
return f"Loop protection triggered - requesting new strategy: {error_msg}"
|
478
492
|
|
479
493
|
error_msg = f"Exception during execution of tool '{tool_name}': {exception}"
|
480
494
|
if self._event_bus:
|
@@ -0,0 +1,83 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: janito
|
3
|
+
Version: 2.30.0
|
4
|
+
Summary: A new Python package called janito.
|
5
|
+
Author-email: João Pinto <janito@ikignosis.org>
|
6
|
+
Project-URL: Homepage, https://github.com/ikignosis/janito
|
7
|
+
Requires-Python: >=3.7
|
8
|
+
Description-Content-Type: text/markdown
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: attrs==25.3.0
|
11
|
+
Requires-Dist: rich==14.0.0
|
12
|
+
Requires-Dist: pathspec==0.12.1
|
13
|
+
Requires-Dist: setuptools>=61.0
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
15
|
+
Requires-Dist: jinja2>=3.0.0
|
16
|
+
Requires-Dist: prompt_toolkit>=3.0.51
|
17
|
+
Requires-Dist: lxml>=5.4.0
|
18
|
+
Requires-Dist: requests>=2.32.4
|
19
|
+
Requires-Dist: bs4>=0.0.2
|
20
|
+
Requires-Dist: questionary>=2.0.1
|
21
|
+
Requires-Dist: openai>=1.68.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
24
|
+
Requires-Dist: pre-commit; extra == "dev"
|
25
|
+
Requires-Dist: ruff==0.11.9; extra == "dev"
|
26
|
+
Requires-Dist: detect-secrets==1.4.0; extra == "dev"
|
27
|
+
Requires-Dist: codespell==2.4.1; extra == "dev"
|
28
|
+
Requires-Dist: black; extra == "dev"
|
29
|
+
Requires-Dist: questionary>=2.0.1; extra == "dev"
|
30
|
+
Requires-Dist: setuptools_scm>=8.0; extra == "dev"
|
31
|
+
Provides-Extra: coder
|
32
|
+
Requires-Dist: janito-coder; extra == "coder"
|
33
|
+
Dynamic: license-file
|
34
|
+
|
35
|
+
# nctl
|
36
|
+
|
37
|
+
```bash
|
38
|
+
$ nctl --help
|
39
|
+
Usage: nctl <command>
|
40
|
+
|
41
|
+
Interact with Nine API resources. See https://docs.nineapis.ch for the full API docs.
|
42
|
+
|
43
|
+
Run "nctl <command> --help" for more information on a command.
|
44
|
+
```
|
45
|
+
|
46
|
+
## Setup
|
47
|
+
|
48
|
+
```bash
|
49
|
+
# If you have go already installed
|
50
|
+
go install github.com/ninech/nctl@latest
|
51
|
+
|
52
|
+
# Homebrew
|
53
|
+
brew install ninech/taps/nctl
|
54
|
+
|
55
|
+
# Debian/Ubuntu
|
56
|
+
echo "deb [trusted=yes] https://repo.nine.ch/deb/ /" | sudo tee /etc/apt/sources.list.d/repo.nine.ch.list
|
57
|
+
sudo apt-get update
|
58
|
+
sudo apt-get install nctl
|
59
|
+
|
60
|
+
# Fedora/RHEL
|
61
|
+
cat <<EOF > /etc/yum.repos.d/repo.nine.ch.repo
|
62
|
+
[repo.nine.ch]
|
63
|
+
name=Nine Repo
|
64
|
+
baseurl=https://repo.nine.ch/yum/
|
65
|
+
enabled=1
|
66
|
+
gpgcheck=0
|
67
|
+
EOF
|
68
|
+
dnf install nctl
|
69
|
+
|
70
|
+
# Arch
|
71
|
+
# Install yay: https://github.com/Jguer/yay#binary
|
72
|
+
yay --version
|
73
|
+
yay -S nctl-bin
|
74
|
+
```
|
75
|
+
|
76
|
+
For Windows users, nctl is also built for arm64 and amd64. You can download the
|
77
|
+
latest exe file from the [releases](https://github.com/ninech/nctl/releases) and
|
78
|
+
install it.
|
79
|
+
|
80
|
+
## Getting started
|
81
|
+
|
82
|
+
* login to the API using `nctl auth login`
|
83
|
+
* run `nctl --help` to get a list of all available commands
|