@craftsamo/git-tools 0.1.0 → 0.2.1
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.
- package/.antigravity-plugin/plugin.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -1
- package/README.md +39 -24
- package/dist/cli.js +167 -61
- package/dist/mcp.js +227 -87
- package/dist/opencode.js +199 -65
- package/package.json +1 -1
- package/plugin.yaml +5 -2
- package/skills/git-push/SKILL.md +2 -2
- package/skills/git-rebase/SKILL.md +2 -2
- package/skills/git-worktree/SKILL.md +1 -0
- package/src/hermes/git_tools_hermes/approval.py +5 -17
- package/src/hermes/git_tools_hermes/plugin.py +2 -2
- package/src/hermes/git_tools_hermes/project.py +4 -3
- package/src/hermes/git_tools_hermes/settings.py +47 -15
- package/src/hermes/git_tools_hermes/tools.py +10 -3
|
@@ -162,18 +162,13 @@ def resolve_target(
|
|
|
162
162
|
"""What ``target`` prints: ``["*"]`` for resources that cannot be told, no description for one absent or bad.
|
|
163
163
|
|
|
164
164
|
``language`` is the plugin's ``language`` setting and ``harness_language`` reads Hermes' display language
|
|
165
|
-
per call; neither may fail the run, and ``target`` picks among them.
|
|
166
|
-
plugin's tool options, which the call's description may read as ``run`` does.
|
|
165
|
+
per call; neither may fail the run, and ``target`` picks among them.
|
|
167
166
|
"""
|
|
168
167
|
try:
|
|
169
168
|
result = runtime.run(
|
|
170
169
|
[
|
|
171
|
-
"target",
|
|
172
|
-
|
|
173
|
-
"--cwd",
|
|
174
|
-
agent_cwd(),
|
|
175
|
-
*language_flags(language, _known(harness_language)),
|
|
176
|
-
*(option_flags or []),
|
|
170
|
+
"target", tool_id, "--cwd", agent_cwd(),
|
|
171
|
+
*language_flags(language, _known(harness_language)), *(option_flags or []),
|
|
177
172
|
],
|
|
178
173
|
stdin=json.dumps(run_arguments(args)),
|
|
179
174
|
timeout=TARGET_TIMEOUT_SECONDS,
|
|
@@ -232,8 +227,7 @@ def make_hook(
|
|
|
232
227
|
|
|
233
228
|
Whether a person is there to ask, and what happens when none is, is
|
|
234
229
|
Hermes' own gate to decide. ``language`` is the plugin's ``language``
|
|
235
|
-
setting; ``harness_language`` is read on every call
|
|
236
|
-
are the flags of the plugin's tool options, passed to ``target``. When ``pins`` is
|
|
230
|
+
setting; ``harness_language`` is read on every call. When ``pins`` is
|
|
237
231
|
given, the pin ``target`` answered is kept in it under the call's
|
|
238
232
|
``pin_key`` for the pin hook; the decision is the same either way.
|
|
239
233
|
"""
|
|
@@ -309,7 +303,6 @@ def register_approval(
|
|
|
309
303
|
) -> None:
|
|
310
304
|
"""Hold every non-read call of ``tools`` for approval, with the card in ``language`` (the plugin's setting).
|
|
311
305
|
|
|
312
|
-
``option_flags`` are the flags of the plugin's tool options, which ``target`` reads as ``run`` does.
|
|
313
306
|
Two hooks, in this order, since Hermes runs them in registration order: the approval hook
|
|
314
307
|
stashes the pin of what the card shows, then the pin hook gives it to the call.
|
|
315
308
|
"""
|
|
@@ -317,12 +310,7 @@ def register_approval(
|
|
|
317
310
|
ctx.register_hook(
|
|
318
311
|
HOOK_NAME,
|
|
319
312
|
make_hook(
|
|
320
|
-
runtime,
|
|
321
|
-
tools,
|
|
322
|
-
surface=lambda: session_surface(ctx),
|
|
323
|
-
language=language,
|
|
324
|
-
pins=pins,
|
|
325
|
-
option_flags=option_flags,
|
|
313
|
+
runtime, tools, surface=lambda: session_surface(ctx), language=language, pins=pins, option_flags=option_flags
|
|
326
314
|
),
|
|
327
315
|
)
|
|
328
316
|
ctx.register_hook(HOOK_NAME, make_pin_hook(tools, pins))
|
|
@@ -9,7 +9,7 @@ from .context import PluginContext
|
|
|
9
9
|
from .listing import list_tools
|
|
10
10
|
from . import project
|
|
11
11
|
from .runtime import Unavailable, locate_runtime
|
|
12
|
-
from .settings import SettingsError, language_setting, option_flags, settings_flags
|
|
12
|
+
from .settings import SettingsError, emoji_setting, language_setting, option_flags, settings_flags
|
|
13
13
|
from .skills import register_skills
|
|
14
14
|
from .tools import register_tools
|
|
15
15
|
|
|
@@ -33,6 +33,6 @@ def register(ctx: PluginContext) -> None:
|
|
|
33
33
|
logger.warning("%s is not available and registers nothing: %s", project.PLUGIN_NAME, error)
|
|
34
34
|
return
|
|
35
35
|
|
|
36
|
-
register_tools(ctx, runtime, tools, flags)
|
|
36
|
+
register_tools(ctx, runtime, tools, flags, emoji_setting(ctx))
|
|
37
37
|
register_approval(ctx, runtime, tools, language_setting(ctx), option_flags(ctx))
|
|
38
38
|
register_skills(ctx, runtime.skills_dir, {tool.toolset for tool in tools})
|
|
@@ -14,9 +14,10 @@ SKILL_PREFIX = "git-"
|
|
|
14
14
|
#: The emoji each toolset is shown with; empty by default until a project adds its own.
|
|
15
15
|
TOOLSET_EMOJI: dict[str, str] = {}
|
|
16
16
|
|
|
17
|
-
#: The settings the tools themselves read
|
|
18
|
-
#:
|
|
19
|
-
OPTIONS
|
|
17
|
+
#: The settings the tools themselves read: each snake_case name and a type (``text``, ``boolean``, ``number``,
|
|
18
|
+
#: ``list``, ``path`` or ``paths``) or a dict with ``type`` and optional bounds (``integer``, ``min``, ``max``),
|
|
19
|
+
#: as in ``src/project.ts``. Write OPTIONS on one line with double quotes. A setting ``a_b`` becomes ``--a-b``.
|
|
20
|
+
OPTIONS: dict[str, str | dict[str, object]] = {"published_text_rules": "text", "worktree_root": "text"}
|
|
20
21
|
|
|
21
22
|
__all__ = [
|
|
22
23
|
"CLI_NAME",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
7
9
|
from . import project
|
|
@@ -31,30 +33,60 @@ def language_setting(ctx: PluginContext) -> str | None:
|
|
|
31
33
|
return (value.strip() or None) if isinstance(value, str) else None
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
def
|
|
35
|
-
"""
|
|
36
|
+
def emoji_setting(ctx: PluginContext, defaults: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
37
|
+
"""The emoji per toolset: ``defaults`` (``project.TOOLSET_EMOJI``) with string entries of ``emoji`` merged over it.
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
``
|
|
39
|
+
A setting that is not a ``dict``, and an entry whose key or value is not a
|
|
40
|
+
``str``, are ignored so a typo never takes every tool away. It is no flag
|
|
41
|
+
of ``settings_flags``.
|
|
42
|
+
"""
|
|
43
|
+
merged = dict(project.TOOLSET_EMOJI if defaults is None else defaults)
|
|
44
|
+
value = ctx.get_config("emoji")
|
|
45
|
+
if isinstance(value, dict):
|
|
46
|
+
merged.update({key: item for key, item in value.items() if isinstance(key, str) and isinstance(item, str)})
|
|
47
|
+
return merged
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
|
|
50
|
+
def _option_flag(name: str, declaration: str | dict[str, object], value: Any) -> list[str]:
|
|
51
|
+
kind = declaration["type"] if isinstance(declaration, dict) else declaration
|
|
52
|
+
flag = f"--{name.replace('_', '-')}"
|
|
53
|
+
if kind in ("text", "path"):
|
|
54
|
+
if not isinstance(value, str):
|
|
55
|
+
expected = "a path" if kind == "path" else "text"
|
|
56
|
+
raise SettingsError(f'setting "{name}" must be {expected}')
|
|
57
|
+
return [flag, text] if (text := value.strip()) else []
|
|
58
|
+
if kind == "boolean":
|
|
59
|
+
if not isinstance(value, bool):
|
|
60
|
+
raise SettingsError(f'setting "{name}" must be true or false')
|
|
61
|
+
return [flag] if value else []
|
|
62
|
+
if kind == "number":
|
|
63
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
64
|
+
raise SettingsError(f'setting "{name}" must be a finite number')
|
|
65
|
+
return [flag, str(value)]
|
|
66
|
+
if kind in ("list", "paths"):
|
|
67
|
+
if not isinstance(value, list) or not all(isinstance(item, str) and "," not in item for item in value):
|
|
68
|
+
expected = "paths" if kind == "paths" else "strings"
|
|
69
|
+
raise SettingsError(f'setting "{name}" must be a list of {expected} without commas')
|
|
70
|
+
items = [item.strip() for item in value if item.strip()]
|
|
71
|
+
return [flag, ",".join(items)] if items else []
|
|
72
|
+
raise SettingsError(f'unknown option type "{kind}" for "{name}"')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def option_flags(ctx: PluginContext, options: Mapping[str, str | dict[str, object]] | None = None) -> list[str]:
|
|
76
|
+
"""The package's tool options as flags, with absent settings left to the command line's environment.
|
|
77
|
+
|
|
78
|
+
Bounds and path absoluteness are checked by the command line, not here.
|
|
43
79
|
"""
|
|
44
80
|
flags: list[str] = []
|
|
45
|
-
for name in project.OPTIONS:
|
|
81
|
+
for name, declaration in (project.OPTIONS if options is None else options).items():
|
|
46
82
|
value = ctx.get_config(name)
|
|
47
|
-
if value is None:
|
|
48
|
-
|
|
49
|
-
if not isinstance(value, str):
|
|
50
|
-
raise SettingsError(f'setting "{name}" must be text')
|
|
51
|
-
if text := value.strip():
|
|
52
|
-
flags += [f"--{name.replace('_', '-')}", text]
|
|
83
|
+
if value is not None:
|
|
84
|
+
flags += _option_flag(name, declaration, value)
|
|
53
85
|
return flags
|
|
54
86
|
|
|
55
87
|
|
|
56
88
|
def settings_flags(ctx: PluginContext) -> list[str]:
|
|
57
|
-
"""Read ``toolsets``, ``read_only``, ``tools
|
|
89
|
+
"""Read ``toolsets``, ``read_only``, ``tools`` and ``exclude_tools`` and return the flags that say the same.
|
|
58
90
|
|
|
59
91
|
A setting that is absent gives no flag, so the command line's own defaults
|
|
60
92
|
(and ``GIT_TOOLS_*`` variables) apply. A wrong type is refused rather
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from .approval import PIN_ARGUMENT, parse_pin, run_arguments
|
|
@@ -76,14 +76,21 @@ def make_handler(runtime: Runtime, tool_id: str, flags: list[str]) -> Callable[.
|
|
|
76
76
|
return handler
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def register_tools(
|
|
79
|
+
def register_tools(
|
|
80
|
+
ctx: PluginContext,
|
|
81
|
+
runtime: Runtime,
|
|
82
|
+
tools: list[ToolInfo],
|
|
83
|
+
flags: list[str],
|
|
84
|
+
emoji: Mapping[str, str] | None = None,
|
|
85
|
+
) -> None:
|
|
80
86
|
"""Register each listed tool under its own ID, grouped in one Hermes toolset per package toolset."""
|
|
87
|
+
toolset_emoji = TOOLSET_EMOJI if emoji is None else emoji
|
|
81
88
|
for tool in tools:
|
|
82
89
|
ctx.register_tool(
|
|
83
90
|
name=tool.id,
|
|
84
91
|
toolset=TOOLSET_PREFIX + tool.toolset.replace("_", "-"),
|
|
85
92
|
schema={"name": tool.id, "description": tool.description, "parameters": tool.input_schema},
|
|
86
93
|
handler=make_handler(runtime, tool.id, flags),
|
|
87
|
-
emoji=
|
|
94
|
+
emoji=toolset_emoji.get(tool.toolset, ""),
|
|
88
95
|
description=tool.title,
|
|
89
96
|
)
|