@craftsamo/git-tools 0.1.0 → 0.2.0
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 +27 -12
- package/dist/cli.js +107 -19
- package/dist/mcp.js +173 -53
- package/dist/opencode.js +133 -17
- package/package.json +1 -1
- package/plugin.yaml +4 -1
- package/skills/git-rebase/SKILL.md +2 -2
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Git tools for coding agents: state, commits, rewrites and stacked branches, worktrees and pushing.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "craftsamo",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"command": "node",
|
|
16
16
|
"args": ["dist/mcp.js"],
|
|
17
17
|
"cwd": ".",
|
|
18
|
+
"default_tools_approval_mode": "writes",
|
|
18
19
|
"env_vars": [
|
|
19
20
|
"GIT_TOOLS_TOOLSETS",
|
|
20
21
|
"GIT_TOOLS_READ_ONLY",
|
package/README.md
CHANGED
|
@@ -96,7 +96,15 @@ codex plugin add git-tools@craft-agent
|
|
|
96
96
|
|
|
97
97
|
Codex turns `-` into `_` in server names, so it shows the tools as
|
|
98
98
|
`mcp__git_tools__git_*`. It starts the MCP server in the plugin's
|
|
99
|
-
install directory
|
|
99
|
+
install directory and passes no roots, so the server takes each call's directory
|
|
100
|
+
from what Codex sends with the call: its sandbox state, which the server asks
|
|
101
|
+
for, else the one workspace it names.
|
|
102
|
+
|
|
103
|
+
The plugin declares `default_tools_approval_mode = "writes"`: Codex asks before
|
|
104
|
+
every tool not marked read-only. A user's
|
|
105
|
+
`[plugins."<plugin>".mcp_servers.<server>]` setting can only tighten it. Setting
|
|
106
|
+
`auto` there yields `prompt` (reads asked too), since Codex treats `auto` and
|
|
107
|
+
`writes` as incomparable.
|
|
100
108
|
|
|
101
109
|
### Grok Build
|
|
102
110
|
|
|
@@ -244,8 +252,10 @@ The toolset is opt-in. The `git-push` skill says how they fit together.
|
|
|
244
252
|
the entry points do. A tool that runs a program is tested against a fake of
|
|
245
253
|
that program on `PATH`; no test reaches a real service.
|
|
246
254
|
3. List it in `src/tools.ts`. A new toolset goes into `toolsets` in
|
|
247
|
-
`src/project.ts` (and `defaultToolsets` when it is on by default),
|
|
248
|
-
|
|
255
|
+
`src/project.ts` (and `defaultToolsets` when it is on by default), a
|
|
256
|
+
setting of the package's own into `options` (and `OPTIONS` in `project.py`
|
|
257
|
+
with `config_schema` in `plugin.yaml`), and a variable the tool reads
|
|
258
|
+
besides the settings into `toolVariables`.
|
|
249
259
|
4. Add or update the toolset's skill, `skills/<prefix>-<toolset>/SKILL.md`,
|
|
250
260
|
whose frontmatter names the toolset under `metadata`.
|
|
251
261
|
5. Run `pnpm run build && pnpm run manifests` to list the tool in
|
|
@@ -272,8 +282,11 @@ Where a write is asked about, and where it is not:
|
|
|
272
282
|
|
|
273
283
|
- **OpenCode** grants any action no rule names, so a write runs unasked unless
|
|
274
284
|
a rule asks for it: add the `git_*` rule shown under [OpenCode](#opencode).
|
|
275
|
-
- **Codex**
|
|
276
|
-
|
|
285
|
+
- **Codex** asks before every write because of the plugin's approval mode.
|
|
286
|
+
Under `codex exec` it refuses with "MCP tool call requires approval, but
|
|
287
|
+
approval policy is never".
|
|
288
|
+
- **Grok Build** under `grok -p` refuses a write it cannot ask about, unless it
|
|
289
|
+
is allowed beforehand.
|
|
277
290
|
- **Grok Build** calls the plugin's tools through its `use_tool` tool, and the
|
|
278
291
|
agent often names a tool without its server first. Grok checks the rules
|
|
279
292
|
against the name as given, so the server-qualified rule alone does not match
|
|
@@ -298,15 +311,17 @@ variable, and an option or setting wins over the variable too.
|
|
|
298
311
|
| `exclude_tools` | `--exclude-tools` | `GIT_TOOLS_EXCLUDE_TOOLS` | single tools to remove by ID; wins over `tools` and `toolsets` |
|
|
299
312
|
| `confirm` | `--confirm` | `GIT_TOOLS_CONFIRM` | MCP server only: `harness` (default) or `elicitation`, as the conventions say |
|
|
300
313
|
| `language` | `--language` | `GIT_TOOLS_LANGUAGE` | native plugins only: the approval prompt's language, such as `ja` |
|
|
314
|
+
| `emoji` | — | — | Hermes only: the emoji shown beside each toolset, by toolset name (`""` for none) |
|
|
301
315
|
| `published_text_rules` | `--published-text-rules` | `GIT_TOOLS_PUBLISHED_TEXT_RULES` | the absolute path of a JSON file of published-text rules (see the push toolset); unset, nothing is checked |
|
|
302
316
|
| `worktree_root` | `--worktree-root` | `GIT_TOOLS_WORKTREE_ROOT` | the absolute path where task worktrees go (`~` allowed); unset, `~/worktrees` |
|
|
303
317
|
|
|
304
|
-
Lists are comma-separated in a flag or a variable.
|
|
305
|
-
`
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
318
|
+
Lists are comma-separated in a flag or a variable. A package's own settings,
|
|
319
|
+
listed in `options` in `src/project.ts`, take a row each here and are read the
|
|
320
|
+
same way: `published_text_rules` and `worktree_root` hold text, trimmed and
|
|
321
|
+
blank meaning unset, and the tools read them themselves. The layout is
|
|
322
|
+
`<root>/<repository>/<branch>`, with each `/` of the branch written `-`; the
|
|
323
|
+
repository is the last part of `origin`'s URL without `.git`, else the folder
|
|
324
|
+
name of the main checkout. Where each harness reads them:
|
|
310
325
|
|
|
311
326
|
- **OpenCode** takes the plugin `options` shown above, with lists as arrays and
|
|
312
327
|
`read_only` as a boolean, and falls back to the variables. V2 runs plugins in
|
|
@@ -358,7 +373,7 @@ else the folder name of the main checkout. Where each harness reads them:
|
|
|
358
373
|
## Layout
|
|
359
374
|
|
|
360
375
|
- `src/project.ts` — the package's name, tool ID prefix, setting prefix,
|
|
361
|
-
toolsets and the other variables its tools read.
|
|
376
|
+
toolsets, tool options and the other variables its tools read.
|
|
362
377
|
- `src/core/` — the tool contract and the tools, free of any harness and of
|
|
363
378
|
the project's names.
|
|
364
379
|
- `src/tools.ts` — the list of every tool, which each entry point registers.
|
package/dist/cli.js
CHANGED
|
@@ -26,8 +26,8 @@ var PROJECT = {
|
|
|
26
26
|
toolsets: ["repo", "commit", "rebase", "worktree", "push"],
|
|
27
27
|
/** The toolsets on when a setting names none. */
|
|
28
28
|
defaultToolsets: ["repo", "commit", "rebase"],
|
|
29
|
-
/** The settings the tools themselves read
|
|
30
|
-
options:
|
|
29
|
+
/** The package's own settings, which the tools themselves read: each snake_case name and its type, with bounds when it needs them. */
|
|
30
|
+
options: { published_text_rules: "text", worktree_root: "text" },
|
|
31
31
|
/** The environment variables the tools read besides the settings, which Codex forwards to the server after them. */
|
|
32
32
|
toolVariables: [
|
|
33
33
|
"LANGUAGE",
|
|
@@ -22994,7 +22994,8 @@ function compileRules(path, entries) {
|
|
|
22994
22994
|
});
|
|
22995
22995
|
}
|
|
22996
22996
|
async function loadPublishedTextRules(options, signal) {
|
|
22997
|
-
const
|
|
22997
|
+
const option = options?.[PUBLISHED_TEXT_RULES_OPTION];
|
|
22998
|
+
const path = typeof option === "string" ? option.trim() : void 0;
|
|
22998
22999
|
if (!path) return [];
|
|
22999
23000
|
if (!isAbsolute4(path)) throw invalidRules(path, "the value must be an absolute path");
|
|
23000
23001
|
return compileRules(path, parseRulesJson(path, await readRulesFile(path, signal)));
|
|
@@ -25349,7 +25350,8 @@ function expandHome(value) {
|
|
|
25349
25350
|
return value.startsWith(HOME_PREFIX) ? join8(homeDirectory(), value.slice(HOME_PREFIX.length)) : value;
|
|
25350
25351
|
}
|
|
25351
25352
|
function worktreeRoot(ctx) {
|
|
25352
|
-
const
|
|
25353
|
+
const option = ctx.options?.[WORKTREE_ROOT_OPTION];
|
|
25354
|
+
const value = typeof option === "string" ? option.trim() : void 0;
|
|
25353
25355
|
if (!value) return join8(homeDirectory(), DEFAULT_ROOT_FOLDER);
|
|
25354
25356
|
const expanded = expandHome(value);
|
|
25355
25357
|
if (!isAbsolute5(expanded)) {
|
|
@@ -26440,6 +26442,8 @@ var PROJECT_PROFILE = {
|
|
|
26440
26442
|
};
|
|
26441
26443
|
|
|
26442
26444
|
// src/entry/settings.ts
|
|
26445
|
+
import { homedir as homedir2 } from "node:os";
|
|
26446
|
+
import { isAbsolute as isAbsolute6, join as join11, resolve as resolvePath, sep as sep3 } from "node:path";
|
|
26443
26447
|
var CONFIRM_MODES = ["harness", "elicitation"];
|
|
26444
26448
|
var ALL_TOOLSETS = "all";
|
|
26445
26449
|
var TRUE_VALUES = ["1", "true"];
|
|
@@ -26451,10 +26455,17 @@ var OPTIONS = {
|
|
|
26451
26455
|
exclude_tools: { flag: "--exclude-tools", envSuffix: "EXCLUDE_TOOLS" },
|
|
26452
26456
|
confirm: { flag: "--confirm", envSuffix: "CONFIRM" }
|
|
26453
26457
|
};
|
|
26458
|
+
function profileOptions(profile) {
|
|
26459
|
+
return profile.options ?? {};
|
|
26460
|
+
}
|
|
26454
26461
|
function settingOptions(profile) {
|
|
26455
|
-
const toolOptions = profile.
|
|
26462
|
+
const toolOptions = Object.entries(profileOptions(profile)).map(([name, spec]) => [
|
|
26456
26463
|
name,
|
|
26457
|
-
{
|
|
26464
|
+
{
|
|
26465
|
+
flag: `--${name.replace(/_/g, "-")}`,
|
|
26466
|
+
envSuffix: name.toUpperCase(),
|
|
26467
|
+
...optionType(spec) === "boolean" && { boolean: true }
|
|
26468
|
+
}
|
|
26458
26469
|
]);
|
|
26459
26470
|
return { ...OPTIONS, ...Object.fromEntries(toolOptions) };
|
|
26460
26471
|
}
|
|
@@ -26462,8 +26473,9 @@ function envName(name, profile) {
|
|
|
26462
26473
|
return `${profile.envPrefix}_${settingOptions(profile)[name].envSuffix}`;
|
|
26463
26474
|
}
|
|
26464
26475
|
function readFlags(argv, profile) {
|
|
26465
|
-
const
|
|
26466
|
-
|
|
26476
|
+
const byFlag = new Map(
|
|
26477
|
+
Object.entries(settingOptions(profile)).map(([name, option]) => [option.flag, { name, ...option }])
|
|
26478
|
+
);
|
|
26467
26479
|
const given = {};
|
|
26468
26480
|
for (let i = 0; i < argv.length; i++) {
|
|
26469
26481
|
const arg = argv[i];
|
|
@@ -26515,19 +26527,103 @@ function parseConfirm(value, source) {
|
|
|
26515
26527
|
}
|
|
26516
26528
|
return mode;
|
|
26517
26529
|
}
|
|
26530
|
+
function parseNumber(value, source) {
|
|
26531
|
+
const trimmed2 = value.trim();
|
|
26532
|
+
const parsed = trimmed2 === "" ? Number.NaN : Number(trimmed2);
|
|
26533
|
+
if (!Number.isFinite(parsed)) {
|
|
26534
|
+
throw new ToolError("invalid_argument", `Unknown number "${value}" in ${source}; use a finite number.`);
|
|
26535
|
+
}
|
|
26536
|
+
return parsed;
|
|
26537
|
+
}
|
|
26538
|
+
function absolutePath(text, source, env) {
|
|
26539
|
+
const home = env.HOME !== void 0 && isAbsolute6(env.HOME) ? env.HOME : homedir2();
|
|
26540
|
+
const expanded = text === "~" || text.startsWith("~/") || text.startsWith(`~${sep3}`) ? join11(home, text.slice(1)) : text;
|
|
26541
|
+
if (!isAbsolute6(expanded)) throw new ToolError("invalid_argument", `${source} must be an absolute path.`);
|
|
26542
|
+
return resolvePath(expanded);
|
|
26543
|
+
}
|
|
26544
|
+
function absolutePaths(items, source, env) {
|
|
26545
|
+
return items.length > 0 ? items.map((item) => absolutePath(item, source, env)) : void 0;
|
|
26546
|
+
}
|
|
26518
26547
|
function envText(name, env, profile) {
|
|
26519
26548
|
const variable = envName(name, profile);
|
|
26520
26549
|
const text = env[variable];
|
|
26521
26550
|
return text === void 0 || text.trim() === "" ? void 0 : { text, source: variable };
|
|
26522
26551
|
}
|
|
26552
|
+
function optionType(spec) {
|
|
26553
|
+
return typeof spec === "string" ? spec : spec.type;
|
|
26554
|
+
}
|
|
26555
|
+
function itemCount(count) {
|
|
26556
|
+
return count === 1 ? "1 item" : `${count} items`;
|
|
26557
|
+
}
|
|
26558
|
+
function numberBoundsError({ integer: integer2, min, max }, source) {
|
|
26559
|
+
const range = min !== void 0 && max !== void 0 ? ` from ${min} to ${max}` : min !== void 0 ? ` of at least ${min}` : max !== void 0 ? ` of at most ${max}` : "";
|
|
26560
|
+
return new ToolError("invalid_argument", `${source} must be ${integer2 ? "a whole number" : "a number"}${range}.`);
|
|
26561
|
+
}
|
|
26562
|
+
function countBoundsError({ min, max }, source) {
|
|
26563
|
+
const range = min !== void 0 && max !== void 0 ? `from ${min} to ${max} items` : min !== void 0 ? `at least ${itemCount(min)}` : `at most ${itemCount(max)}`;
|
|
26564
|
+
return new ToolError("invalid_argument", `${source} must list ${range}.`);
|
|
26565
|
+
}
|
|
26566
|
+
function withinBounds(spec, value, source) {
|
|
26567
|
+
if (value === void 0 || typeof spec === "string") return value;
|
|
26568
|
+
const bounds = spec;
|
|
26569
|
+
const { integer: integer2, min, max } = bounds;
|
|
26570
|
+
if (typeof value === "number") {
|
|
26571
|
+
if (integer2 && !Number.isInteger(value) || min !== void 0 && value < min || max !== void 0 && value > max) {
|
|
26572
|
+
throw numberBoundsError(bounds, source);
|
|
26573
|
+
}
|
|
26574
|
+
} else if (typeof value === "object" && (min !== void 0 && value.length < min || max !== void 0 && value.length > max)) {
|
|
26575
|
+
throw countBoundsError(bounds, source);
|
|
26576
|
+
}
|
|
26577
|
+
return value;
|
|
26578
|
+
}
|
|
26579
|
+
function parseOptionText(spec, given, env) {
|
|
26580
|
+
return withinBounds(spec, parseTypedText(optionType(spec), given, env), given.source);
|
|
26581
|
+
}
|
|
26582
|
+
function parseTypedText(kind, given, env) {
|
|
26583
|
+
switch (kind) {
|
|
26584
|
+
case "text":
|
|
26585
|
+
return nonBlank(given.text);
|
|
26586
|
+
case "boolean":
|
|
26587
|
+
return parseBoolean(given.text, given.source);
|
|
26588
|
+
case "number":
|
|
26589
|
+
return parseNumber(given.text, given.source);
|
|
26590
|
+
case "list": {
|
|
26591
|
+
const items = splitList(given.text);
|
|
26592
|
+
return items.length > 0 ? items : void 0;
|
|
26593
|
+
}
|
|
26594
|
+
case "path": {
|
|
26595
|
+
const text = nonBlank(given.text);
|
|
26596
|
+
return text === void 0 ? void 0 : absolutePath(text, given.source, env);
|
|
26597
|
+
}
|
|
26598
|
+
case "paths":
|
|
26599
|
+
return absolutePaths(splitList(given.text), given.source, env);
|
|
26600
|
+
}
|
|
26601
|
+
}
|
|
26523
26602
|
function optionsFrom(flags, env, profile) {
|
|
26524
26603
|
const options = {};
|
|
26525
|
-
for (const name of profile
|
|
26526
|
-
|
|
26527
|
-
|
|
26604
|
+
for (const [name, spec] of Object.entries(profileOptions(profile))) {
|
|
26605
|
+
if (flags[name] !== void 0) {
|
|
26606
|
+
const parsed = parseOptionText(spec, { text: flags[name], source: settingOptions(profile)[name].flag }, env);
|
|
26607
|
+
if (parsed !== void 0) options[name] = parsed;
|
|
26608
|
+
continue;
|
|
26609
|
+
}
|
|
26610
|
+
const fromEnv = envText(name, env, profile);
|
|
26611
|
+
if (fromEnv !== void 0) {
|
|
26612
|
+
const parsed = parseOptionText(spec, fromEnv, env);
|
|
26613
|
+
if (parsed !== void 0) options[name] = parsed;
|
|
26614
|
+
}
|
|
26528
26615
|
}
|
|
26529
26616
|
return options;
|
|
26530
26617
|
}
|
|
26618
|
+
function parseToolOptions(argv, env, profile = PROJECT_PROFILE) {
|
|
26619
|
+
const flags = readFlags(argv, profile);
|
|
26620
|
+
const known = profileOptions(profile);
|
|
26621
|
+
const other = Object.keys(flags).find((name) => !(name in known));
|
|
26622
|
+
if (other !== void 0) {
|
|
26623
|
+
throw new ToolError("invalid_argument", `Unknown option "${settingOptions(profile)[other].flag}".`);
|
|
26624
|
+
}
|
|
26625
|
+
return optionsFrom(flags, env, profile);
|
|
26626
|
+
}
|
|
26531
26627
|
function parseSettings(argv, env, profile = PROJECT_PROFILE) {
|
|
26532
26628
|
const flags = readFlags(argv, profile);
|
|
26533
26629
|
const value = (name) => flags[name] !== void 0 ? { text: flags[name], source: OPTIONS[name].flag } : envText(name, env, profile);
|
|
@@ -26556,14 +26652,6 @@ function parseToolSettings(argv, env, profile = PROJECT_PROFILE) {
|
|
|
26556
26652
|
);
|
|
26557
26653
|
return settings;
|
|
26558
26654
|
}
|
|
26559
|
-
function parseToolOptions(argv, env, profile = PROJECT_PROFILE) {
|
|
26560
|
-
const flags = readFlags(argv, profile);
|
|
26561
|
-
const other = Object.keys(flags).find((name) => !profile.options.includes(name));
|
|
26562
|
-
if (other !== void 0) {
|
|
26563
|
-
throw new ToolError("invalid_argument", `Unknown option "${settingOptions(profile)[other].flag}".`);
|
|
26564
|
-
}
|
|
26565
|
-
return optionsFrom(flags, env, profile);
|
|
26566
|
-
}
|
|
26567
26655
|
function languageVariable(profile = PROJECT_PROFILE) {
|
|
26568
26656
|
return `${profile.envPrefix}_LANGUAGE`;
|
|
26569
26657
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -29094,6 +29094,10 @@ function packageRoot(bundleUrl) {
|
|
|
29094
29094
|
return dirname2(dirname2(fileURLToPath(bundleUrl)));
|
|
29095
29095
|
}
|
|
29096
29096
|
|
|
29097
|
+
// src/entry/settings.ts
|
|
29098
|
+
import { homedir } from "node:os";
|
|
29099
|
+
import { isAbsolute as isAbsolute2, join as join2, resolve as resolvePath, sep as sep2 } from "node:path";
|
|
29100
|
+
|
|
29097
29101
|
// src/project.ts
|
|
29098
29102
|
var PROJECT = {
|
|
29099
29103
|
/** The plugin, MCP server and program name. */
|
|
@@ -29108,8 +29112,8 @@ var PROJECT = {
|
|
|
29108
29112
|
toolsets: ["repo", "commit", "rebase", "worktree", "push"],
|
|
29109
29113
|
/** The toolsets on when a setting names none. */
|
|
29110
29114
|
defaultToolsets: ["repo", "commit", "rebase"],
|
|
29111
|
-
/** The settings the tools themselves read
|
|
29112
|
-
options:
|
|
29115
|
+
/** The package's own settings, which the tools themselves read: each snake_case name and its type, with bounds when it needs them. */
|
|
29116
|
+
options: { published_text_rules: "text", worktree_root: "text" },
|
|
29113
29117
|
/** The environment variables the tools read besides the settings, which Codex forwards to the server after them. */
|
|
29114
29118
|
toolVariables: [
|
|
29115
29119
|
"LANGUAGE",
|
|
@@ -29169,10 +29173,17 @@ var OPTIONS = {
|
|
|
29169
29173
|
exclude_tools: { flag: "--exclude-tools", envSuffix: "EXCLUDE_TOOLS" },
|
|
29170
29174
|
confirm: { flag: "--confirm", envSuffix: "CONFIRM" }
|
|
29171
29175
|
};
|
|
29176
|
+
function profileOptions(profile) {
|
|
29177
|
+
return profile.options ?? {};
|
|
29178
|
+
}
|
|
29172
29179
|
function settingOptions(profile) {
|
|
29173
|
-
const toolOptions = profile.
|
|
29180
|
+
const toolOptions = Object.entries(profileOptions(profile)).map(([name, spec]) => [
|
|
29174
29181
|
name,
|
|
29175
|
-
{
|
|
29182
|
+
{
|
|
29183
|
+
flag: `--${name.replace(/_/g, "-")}`,
|
|
29184
|
+
envSuffix: name.toUpperCase(),
|
|
29185
|
+
...optionType(spec) === "boolean" && { boolean: true }
|
|
29186
|
+
}
|
|
29176
29187
|
]);
|
|
29177
29188
|
return { ...OPTIONS, ...Object.fromEntries(toolOptions) };
|
|
29178
29189
|
}
|
|
@@ -29180,8 +29191,9 @@ function envName(name, profile) {
|
|
|
29180
29191
|
return `${profile.envPrefix}_${settingOptions(profile)[name].envSuffix}`;
|
|
29181
29192
|
}
|
|
29182
29193
|
function readFlags(argv, profile) {
|
|
29183
|
-
const
|
|
29184
|
-
|
|
29194
|
+
const byFlag = new Map(
|
|
29195
|
+
Object.entries(settingOptions(profile)).map(([name, option]) => [option.flag, { name, ...option }])
|
|
29196
|
+
);
|
|
29185
29197
|
const given = {};
|
|
29186
29198
|
for (let i = 0; i < argv.length; i++) {
|
|
29187
29199
|
const arg = argv[i];
|
|
@@ -29233,16 +29245,91 @@ function parseConfirm(value, source) {
|
|
|
29233
29245
|
}
|
|
29234
29246
|
return mode;
|
|
29235
29247
|
}
|
|
29248
|
+
function parseNumber(value, source) {
|
|
29249
|
+
const trimmed2 = value.trim();
|
|
29250
|
+
const parsed = trimmed2 === "" ? Number.NaN : Number(trimmed2);
|
|
29251
|
+
if (!Number.isFinite(parsed)) {
|
|
29252
|
+
throw new ToolError("invalid_argument", `Unknown number "${value}" in ${source}; use a finite number.`);
|
|
29253
|
+
}
|
|
29254
|
+
return parsed;
|
|
29255
|
+
}
|
|
29256
|
+
function absolutePath(text, source, env) {
|
|
29257
|
+
const home = env.HOME !== void 0 && isAbsolute2(env.HOME) ? env.HOME : homedir();
|
|
29258
|
+
const expanded = text === "~" || text.startsWith("~/") || text.startsWith(`~${sep2}`) ? join2(home, text.slice(1)) : text;
|
|
29259
|
+
if (!isAbsolute2(expanded)) throw new ToolError("invalid_argument", `${source} must be an absolute path.`);
|
|
29260
|
+
return resolvePath(expanded);
|
|
29261
|
+
}
|
|
29262
|
+
function absolutePaths(items, source, env) {
|
|
29263
|
+
return items.length > 0 ? items.map((item) => absolutePath(item, source, env)) : void 0;
|
|
29264
|
+
}
|
|
29236
29265
|
function envText(name, env, profile) {
|
|
29237
29266
|
const variable = envName(name, profile);
|
|
29238
29267
|
const text = env[variable];
|
|
29239
29268
|
return text === void 0 || text.trim() === "" ? void 0 : { text, source: variable };
|
|
29240
29269
|
}
|
|
29270
|
+
function optionType(spec) {
|
|
29271
|
+
return typeof spec === "string" ? spec : spec.type;
|
|
29272
|
+
}
|
|
29273
|
+
function itemCount(count) {
|
|
29274
|
+
return count === 1 ? "1 item" : `${count} items`;
|
|
29275
|
+
}
|
|
29276
|
+
function numberBoundsError({ integer: integer2, min, max }, source) {
|
|
29277
|
+
const range = min !== void 0 && max !== void 0 ? ` from ${min} to ${max}` : min !== void 0 ? ` of at least ${min}` : max !== void 0 ? ` of at most ${max}` : "";
|
|
29278
|
+
return new ToolError("invalid_argument", `${source} must be ${integer2 ? "a whole number" : "a number"}${range}.`);
|
|
29279
|
+
}
|
|
29280
|
+
function countBoundsError({ min, max }, source) {
|
|
29281
|
+
const range = min !== void 0 && max !== void 0 ? `from ${min} to ${max} items` : min !== void 0 ? `at least ${itemCount(min)}` : `at most ${itemCount(max)}`;
|
|
29282
|
+
return new ToolError("invalid_argument", `${source} must list ${range}.`);
|
|
29283
|
+
}
|
|
29284
|
+
function withinBounds(spec, value, source) {
|
|
29285
|
+
if (value === void 0 || typeof spec === "string") return value;
|
|
29286
|
+
const bounds = spec;
|
|
29287
|
+
const { integer: integer2, min, max } = bounds;
|
|
29288
|
+
if (typeof value === "number") {
|
|
29289
|
+
if (integer2 && !Number.isInteger(value) || min !== void 0 && value < min || max !== void 0 && value > max) {
|
|
29290
|
+
throw numberBoundsError(bounds, source);
|
|
29291
|
+
}
|
|
29292
|
+
} else if (typeof value === "object" && (min !== void 0 && value.length < min || max !== void 0 && value.length > max)) {
|
|
29293
|
+
throw countBoundsError(bounds, source);
|
|
29294
|
+
}
|
|
29295
|
+
return value;
|
|
29296
|
+
}
|
|
29297
|
+
function parseOptionText(spec, given, env) {
|
|
29298
|
+
return withinBounds(spec, parseTypedText(optionType(spec), given, env), given.source);
|
|
29299
|
+
}
|
|
29300
|
+
function parseTypedText(kind, given, env) {
|
|
29301
|
+
switch (kind) {
|
|
29302
|
+
case "text":
|
|
29303
|
+
return nonBlank(given.text);
|
|
29304
|
+
case "boolean":
|
|
29305
|
+
return parseBoolean(given.text, given.source);
|
|
29306
|
+
case "number":
|
|
29307
|
+
return parseNumber(given.text, given.source);
|
|
29308
|
+
case "list": {
|
|
29309
|
+
const items = splitList(given.text);
|
|
29310
|
+
return items.length > 0 ? items : void 0;
|
|
29311
|
+
}
|
|
29312
|
+
case "path": {
|
|
29313
|
+
const text = nonBlank(given.text);
|
|
29314
|
+
return text === void 0 ? void 0 : absolutePath(text, given.source, env);
|
|
29315
|
+
}
|
|
29316
|
+
case "paths":
|
|
29317
|
+
return absolutePaths(splitList(given.text), given.source, env);
|
|
29318
|
+
}
|
|
29319
|
+
}
|
|
29241
29320
|
function optionsFrom(flags, env, profile) {
|
|
29242
29321
|
const options = {};
|
|
29243
|
-
for (const name of profile
|
|
29244
|
-
|
|
29245
|
-
|
|
29322
|
+
for (const [name, spec] of Object.entries(profileOptions(profile))) {
|
|
29323
|
+
if (flags[name] !== void 0) {
|
|
29324
|
+
const parsed = parseOptionText(spec, { text: flags[name], source: settingOptions(profile)[name].flag }, env);
|
|
29325
|
+
if (parsed !== void 0) options[name] = parsed;
|
|
29326
|
+
continue;
|
|
29327
|
+
}
|
|
29328
|
+
const fromEnv = envText(name, env, profile);
|
|
29329
|
+
if (fromEnv !== void 0) {
|
|
29330
|
+
const parsed = parseOptionText(spec, fromEnv, env);
|
|
29331
|
+
if (parsed !== void 0) options[name] = parsed;
|
|
29332
|
+
}
|
|
29246
29333
|
}
|
|
29247
29334
|
return options;
|
|
29248
29335
|
}
|
|
@@ -30375,11 +30462,11 @@ import { stat as stat4 } from "node:fs/promises";
|
|
|
30375
30462
|
|
|
30376
30463
|
// src/core/commit/lint.ts
|
|
30377
30464
|
import { readFile, stat as stat3 } from "node:fs/promises";
|
|
30378
|
-
import { join as
|
|
30465
|
+
import { join as join4 } from "node:path";
|
|
30379
30466
|
|
|
30380
30467
|
// src/core/repo/conventions.ts
|
|
30381
30468
|
import { stat as stat2 } from "node:fs/promises";
|
|
30382
|
-
import { join as
|
|
30469
|
+
import { join as join3 } from "node:path";
|
|
30383
30470
|
var DEFAULT_SAMPLE = 30;
|
|
30384
30471
|
var MAX_SAMPLE = 200;
|
|
30385
30472
|
var SUBJECTS_MAX = 15;
|
|
@@ -30421,7 +30508,7 @@ var conventionsSchema = external_exports.object({
|
|
|
30421
30508
|
});
|
|
30422
30509
|
async function isFile(root, path) {
|
|
30423
30510
|
try {
|
|
30424
|
-
return (await stat2(
|
|
30511
|
+
return (await stat2(join3(root, path))).isFile();
|
|
30425
30512
|
} catch {
|
|
30426
30513
|
return false;
|
|
30427
30514
|
}
|
|
@@ -30603,7 +30690,7 @@ async function isFile2(path) {
|
|
|
30603
30690
|
}
|
|
30604
30691
|
}
|
|
30605
30692
|
async function commitlintHook(root, signal) {
|
|
30606
|
-
const candidates = [await gitPath(root, "hooks/commit-msg", signal),
|
|
30693
|
+
const candidates = [await gitPath(root, "hooks/commit-msg", signal), join4(root, HUSKY_HOOK)];
|
|
30607
30694
|
for (const path of candidates) {
|
|
30608
30695
|
let text;
|
|
30609
30696
|
try {
|
|
@@ -30618,10 +30705,10 @@ async function commitlintHook(root, signal) {
|
|
|
30618
30705
|
}
|
|
30619
30706
|
async function commitlintConfigured(root) {
|
|
30620
30707
|
for (const name of COMMITLINT_CONFIG_FILES) {
|
|
30621
|
-
if (await isFile2(
|
|
30708
|
+
if (await isFile2(join4(root, name))) return true;
|
|
30622
30709
|
}
|
|
30623
30710
|
try {
|
|
30624
|
-
const manifest = JSON.parse(await readFile(
|
|
30711
|
+
const manifest = JSON.parse(await readFile(join4(root, "package.json"), "utf8"));
|
|
30625
30712
|
return typeof manifest === "object" && manifest !== null && "commitlint" in manifest;
|
|
30626
30713
|
} catch {
|
|
30627
30714
|
return false;
|
|
@@ -30629,7 +30716,7 @@ async function commitlintConfigured(root) {
|
|
|
30629
30716
|
}
|
|
30630
30717
|
async function commitlint(root, message, signal) {
|
|
30631
30718
|
const hook = await commitlintHook(root, signal);
|
|
30632
|
-
const binary =
|
|
30719
|
+
const binary = join4(root, COMMITLINT_BIN);
|
|
30633
30720
|
if (await isFile2(binary)) {
|
|
30634
30721
|
const result = await exec([binary], {
|
|
30635
30722
|
cwd: root,
|
|
@@ -31192,7 +31279,7 @@ async function readAmendCheck(root, sha, signal) {
|
|
|
31192
31279
|
|
|
31193
31280
|
// src/core/commit/hunks.ts
|
|
31194
31281
|
import { createHash as createHash2 } from "node:crypto";
|
|
31195
|
-
import { isAbsolute as
|
|
31282
|
+
import { isAbsolute as isAbsolute3, relative as relative2, resolve, sep as sep3 } from "node:path";
|
|
31196
31283
|
var MAX_LISTED_HUNKS = 200;
|
|
31197
31284
|
var PREVIEW_LINES = 8;
|
|
31198
31285
|
var DIGEST_LENGTH = 16;
|
|
@@ -31324,7 +31411,7 @@ function diffDigest(diff) {
|
|
|
31324
31411
|
function checkPaths(root, paths) {
|
|
31325
31412
|
for (const path of paths) {
|
|
31326
31413
|
const inside = relative2(root, resolve(root, path));
|
|
31327
|
-
if (inside === ".." || inside.startsWith(`..${
|
|
31414
|
+
if (inside === ".." || inside.startsWith(`..${sep3}`) || isAbsolute3(inside)) {
|
|
31328
31415
|
throw new ToolError(
|
|
31329
31416
|
"invalid_argument",
|
|
31330
31417
|
`${path} is outside the working tree (${root}); name files or directories inside it.`
|
|
@@ -31548,7 +31635,7 @@ var commitStage = defineTool({
|
|
|
31548
31635
|
// src/core/commit/verify.ts
|
|
31549
31636
|
import { mkdtemp, realpath as realpath2, rm, stat as stat5 } from "node:fs/promises";
|
|
31550
31637
|
import { tmpdir } from "node:os";
|
|
31551
|
-
import { isAbsolute as
|
|
31638
|
+
import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
|
|
31552
31639
|
var COMMITS_MAX = 30;
|
|
31553
31640
|
var OUTPUT_TAIL_LINES = 40;
|
|
31554
31641
|
var TIMEOUT_DEFAULT_SEC = 600;
|
|
@@ -31602,7 +31689,7 @@ ${result.stderr}`.trim().split("\n").slice(-OUTPUT_TAIL_LINES).join("\n");
|
|
|
31602
31689
|
}
|
|
31603
31690
|
function checkDir(dir) {
|
|
31604
31691
|
const relative3 = dir?.trim() || ".";
|
|
31605
|
-
if (
|
|
31692
|
+
if (isAbsolute4(relative3) || relative3.split(/[\\/]/).includes("..")) {
|
|
31606
31693
|
throw new ToolError("invalid_argument", "dir must be a relative path inside the repository.");
|
|
31607
31694
|
}
|
|
31608
31695
|
return relative3;
|
|
@@ -31684,14 +31771,14 @@ var commitVerify = defineTool({
|
|
|
31684
31771
|
ctx,
|
|
31685
31772
|
`Run \`${args.command}\` at each of ${commits.length} commits of ${branch} in a throwaway worktree.`
|
|
31686
31773
|
);
|
|
31687
|
-
const parent = await realpath2(await mkdtemp(
|
|
31688
|
-
const tree =
|
|
31774
|
+
const parent = await realpath2(await mkdtemp(join5(tmpdir(), TEMP_DIR_PREFIX)));
|
|
31775
|
+
const tree = join5(parent, "tree");
|
|
31689
31776
|
const results = [];
|
|
31690
31777
|
let setup = null;
|
|
31691
31778
|
try {
|
|
31692
31779
|
await git(["worktree", "add", "--detach", "--quiet", tree, commits[0]], { cwd: root, signal, hooks: false });
|
|
31693
31780
|
const run = async (command) => {
|
|
31694
|
-
const cwd =
|
|
31781
|
+
const cwd = join5(tree, dir);
|
|
31695
31782
|
const present = await stat5(cwd).then(
|
|
31696
31783
|
(stats) => stats.isDirectory(),
|
|
31697
31784
|
() => false
|
|
@@ -31770,7 +31857,7 @@ async function compareBranch(root, remote, selected, expect, signal) {
|
|
|
31770
31857
|
|
|
31771
31858
|
// src/core/push/published-text.ts
|
|
31772
31859
|
import { readFile as readFile2, stat as stat6 } from "node:fs/promises";
|
|
31773
|
-
import { isAbsolute as
|
|
31860
|
+
import { isAbsolute as isAbsolute5 } from "node:path";
|
|
31774
31861
|
var PUBLISHED_TEXT_RULES_OPTION = "published_text_rules";
|
|
31775
31862
|
var MAX_FILE_BYTES = 64 * 1024;
|
|
31776
31863
|
var MAX_RULES = 100;
|
|
@@ -31839,9 +31926,10 @@ function compileRules(path, entries) {
|
|
|
31839
31926
|
});
|
|
31840
31927
|
}
|
|
31841
31928
|
async function loadPublishedTextRules(options, signal) {
|
|
31842
|
-
const
|
|
31929
|
+
const option = options?.[PUBLISHED_TEXT_RULES_OPTION];
|
|
31930
|
+
const path = typeof option === "string" ? option.trim() : void 0;
|
|
31843
31931
|
if (!path) return [];
|
|
31844
|
-
if (!
|
|
31932
|
+
if (!isAbsolute5(path)) throw invalidRules(path, "the value must be an absolute path");
|
|
31845
31933
|
return compileRules(path, parseRulesJson(path, await readRulesFile(path, signal)));
|
|
31846
31934
|
}
|
|
31847
31935
|
var publishedTextFindingSchema = external_exports.object({
|
|
@@ -32796,11 +32884,11 @@ var rebaseRead = defineTool({
|
|
|
32796
32884
|
// src/core/rebase/resume.ts
|
|
32797
32885
|
import { createHash as createHash3 } from "node:crypto";
|
|
32798
32886
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
32799
|
-
import { join as
|
|
32887
|
+
import { join as join8 } from "node:path";
|
|
32800
32888
|
|
|
32801
32889
|
// src/core/conflicts.ts
|
|
32802
32890
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
32803
|
-
import { basename as basename2, join as
|
|
32891
|
+
import { basename as basename2, join as join6 } from "node:path";
|
|
32804
32892
|
var REGION_MAX = 20;
|
|
32805
32893
|
var SIDE_LINES_MAX = 40;
|
|
32806
32894
|
var LOCKFILES = {
|
|
@@ -32912,7 +33000,7 @@ async function readConflictFile(root, path, xy, operation) {
|
|
|
32912
33000
|
}
|
|
32913
33001
|
let text;
|
|
32914
33002
|
try {
|
|
32915
|
-
text = await readFile3(
|
|
33003
|
+
text = await readFile3(join6(root, path), "utf8");
|
|
32916
33004
|
} catch {
|
|
32917
33005
|
return { ...file2, region_count: 0, regions: [] };
|
|
32918
33006
|
}
|
|
@@ -32995,13 +33083,13 @@ var rebaseOutcomeSchema = external_exports.object({
|
|
|
32995
33083
|
|
|
32996
33084
|
// src/core/rebase/rebase-state.ts
|
|
32997
33085
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
32998
|
-
import { join as
|
|
33086
|
+
import { join as join7 } from "node:path";
|
|
32999
33087
|
var HEADS_PREFIX = "refs/heads/";
|
|
33000
33088
|
var STATE_DIRS = ["rebase-merge", "rebase-apply"];
|
|
33001
33089
|
var UPDATE_REFS_LINES = 3;
|
|
33002
33090
|
async function readStateFile(dir, name) {
|
|
33003
33091
|
try {
|
|
33004
|
-
return (await readFile4(
|
|
33092
|
+
return (await readFile4(join7(dir, name), "utf8")).trim();
|
|
33005
33093
|
} catch {
|
|
33006
33094
|
return void 0;
|
|
33007
33095
|
}
|
|
@@ -33176,7 +33264,7 @@ async function conflictedFiles(root, signal) {
|
|
|
33176
33264
|
for (const path of await unmergedPaths(root, signal)) {
|
|
33177
33265
|
let content;
|
|
33178
33266
|
try {
|
|
33179
|
-
content = await readFile5(
|
|
33267
|
+
content = await readFile5(join8(root, path));
|
|
33180
33268
|
} catch {
|
|
33181
33269
|
content = null;
|
|
33182
33270
|
}
|
|
@@ -34175,8 +34263,8 @@ var repoRead = defineTool({
|
|
|
34175
34263
|
import { lstat } from "node:fs/promises";
|
|
34176
34264
|
|
|
34177
34265
|
// src/core/worktree/layout.ts
|
|
34178
|
-
import { homedir } from "node:os";
|
|
34179
|
-
import { basename as basename3, dirname as dirname3, isAbsolute as
|
|
34266
|
+
import { homedir as homedir2 } from "node:os";
|
|
34267
|
+
import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute6, join as join9, resolve as resolve2 } from "node:path";
|
|
34180
34268
|
var WORKTREE_ROOT_OPTION = "worktree_root";
|
|
34181
34269
|
var DEFAULT_ROOT_FOLDER = "worktrees";
|
|
34182
34270
|
var HOME_MARK = "~";
|
|
@@ -34187,17 +34275,18 @@ var URL_SEPARATOR = /[/:]/;
|
|
|
34187
34275
|
var TRAILING_SLASHES = /\/+$/;
|
|
34188
34276
|
var NOT_A_NAME = [".", ".."];
|
|
34189
34277
|
function homeDirectory() {
|
|
34190
|
-
return process.env.HOME?.trim() ||
|
|
34278
|
+
return process.env.HOME?.trim() || homedir2();
|
|
34191
34279
|
}
|
|
34192
34280
|
function expandHome(value) {
|
|
34193
34281
|
if (value === HOME_MARK) return homeDirectory();
|
|
34194
|
-
return value.startsWith(HOME_PREFIX) ?
|
|
34282
|
+
return value.startsWith(HOME_PREFIX) ? join9(homeDirectory(), value.slice(HOME_PREFIX.length)) : value;
|
|
34195
34283
|
}
|
|
34196
34284
|
function worktreeRoot(ctx) {
|
|
34197
|
-
const
|
|
34198
|
-
|
|
34285
|
+
const option = ctx.options?.[WORKTREE_ROOT_OPTION];
|
|
34286
|
+
const value = typeof option === "string" ? option.trim() : void 0;
|
|
34287
|
+
if (!value) return join9(homeDirectory(), DEFAULT_ROOT_FOLDER);
|
|
34199
34288
|
const expanded = expandHome(value);
|
|
34200
|
-
if (!
|
|
34289
|
+
if (!isAbsolute6(expanded)) {
|
|
34201
34290
|
throw new ToolError(
|
|
34202
34291
|
"invalid_argument",
|
|
34203
34292
|
`The ${WORKTREE_ROOT_OPTION} setting (${value}): the value must be an absolute path, or start with ${HOME_PREFIX}.`
|
|
@@ -34219,7 +34308,7 @@ async function repositoryName(root, signal) {
|
|
|
34219
34308
|
return fromUrl || basename3(await mainCheckout(root, signal)).replace(GIT_SUFFIX, "");
|
|
34220
34309
|
}
|
|
34221
34310
|
function worktreePath(root, repository, branch) {
|
|
34222
|
-
return
|
|
34311
|
+
return join9(root, repository, branch.replaceAll("/", "-"));
|
|
34223
34312
|
}
|
|
34224
34313
|
|
|
34225
34314
|
// src/core/worktree/add-plan.ts
|
|
@@ -34482,7 +34571,7 @@ var worktreeAdd = defineTool({
|
|
|
34482
34571
|
});
|
|
34483
34572
|
|
|
34484
34573
|
// src/core/worktree/list.ts
|
|
34485
|
-
import { dirname as dirname5, join as
|
|
34574
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
34486
34575
|
var worktreeSchema = external_exports.object({
|
|
34487
34576
|
directory: external_exports.string().describe("The worktree's directory: its real path when it exists, else as git prints it."),
|
|
34488
34577
|
branch: external_exports.string().nullable().describe("The checked-out branch's name; null when HEAD is detached or bare."),
|
|
@@ -34517,7 +34606,7 @@ async function readWorktreeList(callerRoot, root, signal) {
|
|
|
34517
34606
|
const repository = await repositoryName(callerRoot, signal);
|
|
34518
34607
|
const [caller, layoutFolder, entries] = await Promise.all([
|
|
34519
34608
|
realpathOrSelf(callerRoot),
|
|
34520
|
-
realpathOfExisting(
|
|
34609
|
+
realpathOfExisting(join10(root, repository)),
|
|
34521
34610
|
listWorktrees(callerRoot, signal)
|
|
34522
34611
|
]);
|
|
34523
34612
|
return {
|
|
@@ -34696,7 +34785,7 @@ async function planRemove(request) {
|
|
|
34696
34785
|
|
|
34697
34786
|
// src/core/worktree/remove-run.ts
|
|
34698
34787
|
import { rmdir } from "node:fs/promises";
|
|
34699
|
-
import { dirname as dirname6, join as
|
|
34788
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
34700
34789
|
|
|
34701
34790
|
// src/core/worktree/landed.ts
|
|
34702
34791
|
var LANDED_READ_TIMEOUT_MS = PROTECTION_READ_TIMEOUT_MS;
|
|
@@ -34778,7 +34867,7 @@ async function removeWorktreeRecord(request, plan) {
|
|
|
34778
34867
|
}
|
|
34779
34868
|
async function removeEmptyLayoutFolder(request, plan) {
|
|
34780
34869
|
const { callerRoot, worktreeRoot: worktreeRoot2, signal } = request;
|
|
34781
|
-
const folder = await realpathOfExisting(
|
|
34870
|
+
const folder = await realpathOfExisting(join11(worktreeRoot2, await repositoryName(callerRoot, signal)));
|
|
34782
34871
|
const parent = dirname6(plan.directory);
|
|
34783
34872
|
if (await realpathOfExisting(parent) !== folder) return;
|
|
34784
34873
|
try {
|
|
@@ -36699,23 +36788,52 @@ function elicitationConfirm(server, signal) {
|
|
|
36699
36788
|
|
|
36700
36789
|
// src/mcp/roots.ts
|
|
36701
36790
|
import { statSync } from "node:fs";
|
|
36791
|
+
import { isAbsolute as isAbsolute7 } from "node:path";
|
|
36702
36792
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
36703
36793
|
var FILE_URI_PROTOCOL = "file:";
|
|
36794
|
+
var SANDBOX_STATE_META = "codex/sandbox-state-meta";
|
|
36795
|
+
var TURN_METADATA = "x-codex-turn-metadata";
|
|
36796
|
+
function existingDirectory(dir) {
|
|
36797
|
+
try {
|
|
36798
|
+
return statSync(dir, { throwIfNoEntry: false })?.isDirectory() ? dir : void 0;
|
|
36799
|
+
} catch {
|
|
36800
|
+
return void 0;
|
|
36801
|
+
}
|
|
36802
|
+
}
|
|
36704
36803
|
function localDirectory(uri) {
|
|
36705
36804
|
try {
|
|
36706
36805
|
const url2 = new URL(uri);
|
|
36707
|
-
|
|
36708
|
-
const dir = fileURLToPath2(url2);
|
|
36709
|
-
return statSync(dir, { throwIfNoEntry: false })?.isDirectory() ? dir : void 0;
|
|
36806
|
+
return url2.protocol === FILE_URI_PROTOCOL ? existingDirectory(fileURLToPath2(url2)) : void 0;
|
|
36710
36807
|
} catch {
|
|
36711
36808
|
return void 0;
|
|
36712
36809
|
}
|
|
36713
36810
|
}
|
|
36811
|
+
function isRecord(value) {
|
|
36812
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36813
|
+
}
|
|
36814
|
+
async function usableDirectory(candidate, ownDir, allowPath = false) {
|
|
36815
|
+
if (typeof candidate !== "string") return void 0;
|
|
36816
|
+
const dir = allowPath && isAbsolute7(candidate) ? existingDirectory(candidate) : localDirectory(candidate);
|
|
36817
|
+
if (dir === void 0 || ownDir !== void 0 && await isWithin(dir, ownDir)) return void 0;
|
|
36818
|
+
return dir;
|
|
36819
|
+
}
|
|
36820
|
+
async function callDirectory(meta3, ownDir) {
|
|
36821
|
+
if (!isRecord(meta3)) return void 0;
|
|
36822
|
+
const sandbox = meta3[SANDBOX_STATE_META];
|
|
36823
|
+
if (isRecord(sandbox)) {
|
|
36824
|
+
const dir = await usableDirectory(sandbox.sandboxCwd, ownDir, true);
|
|
36825
|
+
if (dir !== void 0) return dir;
|
|
36826
|
+
}
|
|
36827
|
+
const turn = meta3[TURN_METADATA];
|
|
36828
|
+
if (!isRecord(turn) || !isRecord(turn.workspaces)) return void 0;
|
|
36829
|
+
const workspaces = Object.keys(turn.workspaces);
|
|
36830
|
+
return workspaces.length === 1 ? usableDirectory(workspaces[0], ownDir, true) : void 0;
|
|
36831
|
+
}
|
|
36714
36832
|
async function firstUsableRoot(server, ownDir, signal) {
|
|
36715
36833
|
const { roots } = await server.listRoots(void 0, { signal });
|
|
36716
36834
|
for (const root of roots) {
|
|
36717
|
-
const dir =
|
|
36718
|
-
if (dir !== void 0
|
|
36835
|
+
const dir = await usableDirectory(root.uri, ownDir);
|
|
36836
|
+
if (dir !== void 0) return dir;
|
|
36719
36837
|
}
|
|
36720
36838
|
return void 0;
|
|
36721
36839
|
}
|
|
@@ -36726,7 +36844,9 @@ function createWorkingDirResolver(server, fallback, ownDir) {
|
|
|
36726
36844
|
cached2 = void 0;
|
|
36727
36845
|
generation++;
|
|
36728
36846
|
});
|
|
36729
|
-
return async (signal) => {
|
|
36847
|
+
return async (signal, meta3) => {
|
|
36848
|
+
const callDir = await callDirectory(meta3, ownDir);
|
|
36849
|
+
if (callDir !== void 0) return callDir;
|
|
36730
36850
|
if (!server.getClientCapabilities()?.roots) return fallback();
|
|
36731
36851
|
if (cached2 !== void 0) return cached2;
|
|
36732
36852
|
const started = generation;
|
|
@@ -36767,7 +36887,7 @@ function createServer(specs, settings, options = {}) {
|
|
|
36767
36887
|
`));
|
|
36768
36888
|
const server = new Server(
|
|
36769
36889
|
{ name: PROJECT.name, version: options.version ?? DEFAULT_VERSION },
|
|
36770
|
-
{ capabilities: { tools: {} } }
|
|
36890
|
+
{ capabilities: { tools: {}, experimental: { [SANDBOX_STATE_META]: {} } } }
|
|
36771
36891
|
);
|
|
36772
36892
|
const workingDir = createWorkingDirResolver(server, options.cwd ?? (() => process.cwd()), options.ownDir);
|
|
36773
36893
|
const toolsets = options.toolsets ?? PROJECT_PROFILE.toolsets;
|
|
@@ -36780,7 +36900,7 @@ function createServer(specs, settings, options = {}) {
|
|
|
36780
36900
|
try {
|
|
36781
36901
|
const confirm = settings.confirm === "elicitation" ? elicitationConfirm(server, extra.signal) : void 0;
|
|
36782
36902
|
const ctx = {
|
|
36783
|
-
cwd: await workingDir(extra.signal),
|
|
36903
|
+
cwd: await workingDir(extra.signal, request.params._meta),
|
|
36784
36904
|
ownDir: options.ownDir,
|
|
36785
36905
|
options: settings.options,
|
|
36786
36906
|
signal: extra.signal,
|
|
@@ -36808,7 +36928,7 @@ async function main() {
|
|
|
36808
36928
|
const settings = parseSettings(process.argv.slice(2), process.env);
|
|
36809
36929
|
server = createServer(ALL_TOOLS, settings, {
|
|
36810
36930
|
ownDir: packageRoot(import.meta.url),
|
|
36811
|
-
version: true ? "0.
|
|
36931
|
+
version: true ? "0.2.0" : void 0
|
|
36812
36932
|
});
|
|
36813
36933
|
} catch (error62) {
|
|
36814
36934
|
if (!isToolError(error62)) throw error62;
|
package/dist/opencode.js
CHANGED
|
@@ -25,8 +25,8 @@ var PROJECT = {
|
|
|
25
25
|
toolsets: ["repo", "commit", "rebase", "worktree", "push"],
|
|
26
26
|
/** The toolsets on when a setting names none. */
|
|
27
27
|
defaultToolsets: ["repo", "commit", "rebase"],
|
|
28
|
-
/** The settings the tools themselves read
|
|
29
|
-
options:
|
|
28
|
+
/** The package's own settings, which the tools themselves read: each snake_case name and its type, with bounds when it needs them. */
|
|
29
|
+
options: { published_text_rules: "text", worktree_root: "text" },
|
|
30
30
|
/** The environment variables the tools read besides the settings, which Codex forwards to the server after them. */
|
|
31
31
|
toolVariables: [
|
|
32
32
|
"LANGUAGE",
|
|
@@ -22980,7 +22980,8 @@ function compileRules(path, entries) {
|
|
|
22980
22980
|
});
|
|
22981
22981
|
}
|
|
22982
22982
|
async function loadPublishedTextRules(options, signal) {
|
|
22983
|
-
const
|
|
22983
|
+
const option = options?.[PUBLISHED_TEXT_RULES_OPTION];
|
|
22984
|
+
const path = typeof option === "string" ? option.trim() : void 0;
|
|
22984
22985
|
if (!path) return [];
|
|
22985
22986
|
if (!isAbsolute4(path)) throw invalidRules(path, "the value must be an absolute path");
|
|
22986
22987
|
return compileRules(path, parseRulesJson(path, await readRulesFile(path, signal)));
|
|
@@ -25335,7 +25336,8 @@ function expandHome(value) {
|
|
|
25335
25336
|
return value.startsWith(HOME_PREFIX) ? join8(homeDirectory(), value.slice(HOME_PREFIX.length)) : value;
|
|
25336
25337
|
}
|
|
25337
25338
|
function worktreeRoot(ctx) {
|
|
25338
|
-
const
|
|
25339
|
+
const option = ctx.options?.[WORKTREE_ROOT_OPTION];
|
|
25340
|
+
const value = typeof option === "string" ? option.trim() : void 0;
|
|
25339
25341
|
if (!value) return join8(homeDirectory(), DEFAULT_ROOT_FOLDER);
|
|
25340
25342
|
const expanded = expandHome(value);
|
|
25341
25343
|
if (!isAbsolute5(expanded)) {
|
|
@@ -26454,6 +26456,8 @@ var PROJECT_PROFILE = {
|
|
|
26454
26456
|
};
|
|
26455
26457
|
|
|
26456
26458
|
// src/entry/settings.ts
|
|
26459
|
+
import { homedir as homedir2 } from "node:os";
|
|
26460
|
+
import { isAbsolute as isAbsolute6, join as join11, resolve as resolvePath, sep as sep3 } from "node:path";
|
|
26457
26461
|
var ALL_TOOLSETS = "all";
|
|
26458
26462
|
var TRUE_VALUES = ["1", "true"];
|
|
26459
26463
|
var FALSE_VALUES = ["0", "false"];
|
|
@@ -26464,10 +26468,17 @@ var OPTIONS = {
|
|
|
26464
26468
|
exclude_tools: { flag: "--exclude-tools", envSuffix: "EXCLUDE_TOOLS" },
|
|
26465
26469
|
confirm: { flag: "--confirm", envSuffix: "CONFIRM" }
|
|
26466
26470
|
};
|
|
26471
|
+
function profileOptions(profile) {
|
|
26472
|
+
return profile.options ?? {};
|
|
26473
|
+
}
|
|
26467
26474
|
function settingOptions(profile) {
|
|
26468
|
-
const toolOptions = profile.
|
|
26475
|
+
const toolOptions = Object.entries(profileOptions(profile)).map(([name, spec]) => [
|
|
26469
26476
|
name,
|
|
26470
|
-
{
|
|
26477
|
+
{
|
|
26478
|
+
flag: `--${name.replace(/_/g, "-")}`,
|
|
26479
|
+
envSuffix: name.toUpperCase(),
|
|
26480
|
+
...optionType(spec) === "boolean" && { boolean: true }
|
|
26481
|
+
}
|
|
26471
26482
|
]);
|
|
26472
26483
|
return { ...OPTIONS, ...Object.fromEntries(toolOptions) };
|
|
26473
26484
|
}
|
|
@@ -26495,11 +26506,78 @@ function parseBoolean(value, source) {
|
|
|
26495
26506
|
`Unknown value "${value}" in ${source}; use one of ${[...TRUE_VALUES, ...FALSE_VALUES].join(", ")}.`
|
|
26496
26507
|
);
|
|
26497
26508
|
}
|
|
26509
|
+
function parseNumber(value, source) {
|
|
26510
|
+
const trimmed2 = value.trim();
|
|
26511
|
+
const parsed = trimmed2 === "" ? Number.NaN : Number(trimmed2);
|
|
26512
|
+
if (!Number.isFinite(parsed)) {
|
|
26513
|
+
throw new ToolError("invalid_argument", `Unknown number "${value}" in ${source}; use a finite number.`);
|
|
26514
|
+
}
|
|
26515
|
+
return parsed;
|
|
26516
|
+
}
|
|
26517
|
+
function absolutePath(text, source, env) {
|
|
26518
|
+
const home = env.HOME !== void 0 && isAbsolute6(env.HOME) ? env.HOME : homedir2();
|
|
26519
|
+
const expanded = text === "~" || text.startsWith("~/") || text.startsWith(`~${sep3}`) ? join11(home, text.slice(1)) : text;
|
|
26520
|
+
if (!isAbsolute6(expanded)) throw new ToolError("invalid_argument", `${source} must be an absolute path.`);
|
|
26521
|
+
return resolvePath(expanded);
|
|
26522
|
+
}
|
|
26523
|
+
function absolutePaths(items, source, env) {
|
|
26524
|
+
return items.length > 0 ? items.map((item) => absolutePath(item, source, env)) : void 0;
|
|
26525
|
+
}
|
|
26498
26526
|
function envText(name, env, profile) {
|
|
26499
26527
|
const variable = envName(name, profile);
|
|
26500
26528
|
const text = env[variable];
|
|
26501
26529
|
return text === void 0 || text.trim() === "" ? void 0 : { text, source: variable };
|
|
26502
26530
|
}
|
|
26531
|
+
function optionType(spec) {
|
|
26532
|
+
return typeof spec === "string" ? spec : spec.type;
|
|
26533
|
+
}
|
|
26534
|
+
function itemCount(count) {
|
|
26535
|
+
return count === 1 ? "1 item" : `${count} items`;
|
|
26536
|
+
}
|
|
26537
|
+
function numberBoundsError({ integer: integer2, min, max }, source) {
|
|
26538
|
+
const range = min !== void 0 && max !== void 0 ? ` from ${min} to ${max}` : min !== void 0 ? ` of at least ${min}` : max !== void 0 ? ` of at most ${max}` : "";
|
|
26539
|
+
return new ToolError("invalid_argument", `${source} must be ${integer2 ? "a whole number" : "a number"}${range}.`);
|
|
26540
|
+
}
|
|
26541
|
+
function countBoundsError({ min, max }, source) {
|
|
26542
|
+
const range = min !== void 0 && max !== void 0 ? `from ${min} to ${max} items` : min !== void 0 ? `at least ${itemCount(min)}` : `at most ${itemCount(max)}`;
|
|
26543
|
+
return new ToolError("invalid_argument", `${source} must list ${range}.`);
|
|
26544
|
+
}
|
|
26545
|
+
function withinBounds(spec, value, source) {
|
|
26546
|
+
if (value === void 0 || typeof spec === "string") return value;
|
|
26547
|
+
const bounds = spec;
|
|
26548
|
+
const { integer: integer2, min, max } = bounds;
|
|
26549
|
+
if (typeof value === "number") {
|
|
26550
|
+
if (integer2 && !Number.isInteger(value) || min !== void 0 && value < min || max !== void 0 && value > max) {
|
|
26551
|
+
throw numberBoundsError(bounds, source);
|
|
26552
|
+
}
|
|
26553
|
+
} else if (typeof value === "object" && (min !== void 0 && value.length < min || max !== void 0 && value.length > max)) {
|
|
26554
|
+
throw countBoundsError(bounds, source);
|
|
26555
|
+
}
|
|
26556
|
+
return value;
|
|
26557
|
+
}
|
|
26558
|
+
function parseOptionText(spec, given, env) {
|
|
26559
|
+
return withinBounds(spec, parseTypedText(optionType(spec), given, env), given.source);
|
|
26560
|
+
}
|
|
26561
|
+
function parseTypedText(kind, given, env) {
|
|
26562
|
+
switch (kind) {
|
|
26563
|
+
case "text":
|
|
26564
|
+
return nonBlank(given.text);
|
|
26565
|
+
case "boolean":
|
|
26566
|
+
return parseBoolean(given.text, given.source);
|
|
26567
|
+
case "number":
|
|
26568
|
+
return parseNumber(given.text, given.source);
|
|
26569
|
+
case "list": {
|
|
26570
|
+
const items = splitList(given.text);
|
|
26571
|
+
return items.length > 0 ? items : void 0;
|
|
26572
|
+
}
|
|
26573
|
+
case "path": {
|
|
26574
|
+
const text = nonBlank(given.text);
|
|
26575
|
+
return text === void 0 ? void 0 : absolutePath(text, given.source, env);
|
|
26576
|
+
}
|
|
26577
|
+
case "paths":
|
|
26578
|
+
return absolutePaths(splitList(given.text), given.source, env);
|
|
26579
|
+
}
|
|
26580
|
+
}
|
|
26503
26581
|
var TOOL_OPTION_NAMES = ["toolsets", "read_only", "tools", "exclude_tools"];
|
|
26504
26582
|
function stringList(value) {
|
|
26505
26583
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
@@ -26526,7 +26604,7 @@ function listOption(name) {
|
|
|
26526
26604
|
}
|
|
26527
26605
|
var LANGUAGE_OPTION = "language";
|
|
26528
26606
|
function pluginOptionNames(profile) {
|
|
26529
|
-
return [...TOOL_OPTION_NAMES, LANGUAGE_OPTION, ...profile
|
|
26607
|
+
return [...TOOL_OPTION_NAMES, LANGUAGE_OPTION, ...Object.keys(profileOptions(profile))];
|
|
26530
26608
|
}
|
|
26531
26609
|
function languageVariable(profile = PROJECT_PROFILE) {
|
|
26532
26610
|
return `${profile.envPrefix}_LANGUAGE`;
|
|
@@ -26542,9 +26620,41 @@ function languageOption(value) {
|
|
|
26542
26620
|
if (typeof value !== "string") throw optionError(LANGUAGE_OPTION, 'a language tag such as "ja"');
|
|
26543
26621
|
return nonBlank(value);
|
|
26544
26622
|
}
|
|
26545
|
-
function
|
|
26546
|
-
|
|
26547
|
-
|
|
26623
|
+
function toolOption(name, spec, value, env) {
|
|
26624
|
+
return withinBounds(spec, toolOptionOfType(name, optionType(spec), value, env), `Option "${name}"`);
|
|
26625
|
+
}
|
|
26626
|
+
function toolOptionOfType(name, kind, value, env) {
|
|
26627
|
+
switch (kind) {
|
|
26628
|
+
case "text":
|
|
26629
|
+
if (typeof value !== "string") throw optionError(name, "text");
|
|
26630
|
+
return nonBlank(value);
|
|
26631
|
+
case "boolean":
|
|
26632
|
+
if (typeof value !== "boolean") throw optionError(name, "true or false");
|
|
26633
|
+
return value;
|
|
26634
|
+
case "number":
|
|
26635
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw optionError(name, "a finite number");
|
|
26636
|
+
return value;
|
|
26637
|
+
case "list": {
|
|
26638
|
+
const list = stringList(value);
|
|
26639
|
+
if (!list) throw optionError(name, "a list of strings");
|
|
26640
|
+
const items = list.map((item) => item.trim()).filter((item) => item !== "");
|
|
26641
|
+
return items.length > 0 ? items : void 0;
|
|
26642
|
+
}
|
|
26643
|
+
case "path": {
|
|
26644
|
+
if (typeof value !== "string") throw optionError(name, "an absolute path");
|
|
26645
|
+
const text = nonBlank(value);
|
|
26646
|
+
return text === void 0 ? void 0 : absolutePath(text, `Option "${name}"`, env);
|
|
26647
|
+
}
|
|
26648
|
+
case "paths": {
|
|
26649
|
+
const list = stringList(value);
|
|
26650
|
+
if (!list) throw optionError(name, "a list of absolute paths");
|
|
26651
|
+
return absolutePaths(
|
|
26652
|
+
list.flatMap((item) => nonBlank(item) ?? []),
|
|
26653
|
+
`Option "${name}"`,
|
|
26654
|
+
env
|
|
26655
|
+
);
|
|
26656
|
+
}
|
|
26657
|
+
}
|
|
26548
26658
|
}
|
|
26549
26659
|
function settingsFromOptions(options, env, profile = PROJECT_PROFILE) {
|
|
26550
26660
|
const given = options ?? {};
|
|
@@ -26558,12 +26668,18 @@ function settingsFromOptions(options, env, profile = PROJECT_PROFILE) {
|
|
|
26558
26668
|
const text = envText(name, env, profile);
|
|
26559
26669
|
return text !== void 0 ? fromText(text) : void 0;
|
|
26560
26670
|
};
|
|
26561
|
-
const
|
|
26562
|
-
for (const name of profile
|
|
26563
|
-
|
|
26564
|
-
if (
|
|
26671
|
+
const parsedOptions = {};
|
|
26672
|
+
for (const [name, spec] of Object.entries(profileOptions(profile))) {
|
|
26673
|
+
let parsed;
|
|
26674
|
+
if (given[name] !== void 0) {
|
|
26675
|
+
parsed = toolOption(name, spec, given[name], env);
|
|
26676
|
+
} else {
|
|
26677
|
+
const fromEnv = envText(name, env, profile);
|
|
26678
|
+
if (fromEnv !== void 0) parsed = parseOptionText(spec, fromEnv, env);
|
|
26679
|
+
}
|
|
26680
|
+
if (parsed !== void 0) parsedOptions[name] = parsed;
|
|
26565
26681
|
}
|
|
26566
|
-
const settings = { options:
|
|
26682
|
+
const settings = { options: parsedOptions };
|
|
26567
26683
|
const toolsets = resolve5(
|
|
26568
26684
|
"toolsets",
|
|
26569
26685
|
(value) => toolsetsOption(value, profile.toolsets),
|
|
@@ -26669,7 +26785,7 @@ function createV1Server(specs, deps = {}) {
|
|
|
26669
26785
|
|
|
26670
26786
|
// src/opencode/skills.ts
|
|
26671
26787
|
import { readFile as readFile6, readdir } from "node:fs/promises";
|
|
26672
|
-
import { join as
|
|
26788
|
+
import { join as join12, resolve as resolve4 } from "node:path";
|
|
26673
26789
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
26674
26790
|
var SKILL_FILE = "SKILL.md";
|
|
26675
26791
|
var FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/;
|
|
@@ -26770,7 +26886,7 @@ async function readSkills(dir) {
|
|
|
26770
26886
|
}
|
|
26771
26887
|
const skills = /* @__PURE__ */ new Map();
|
|
26772
26888
|
for (const directory of entries.sort()) {
|
|
26773
|
-
const path =
|
|
26889
|
+
const path = join12(root, directory, SKILL_FILE);
|
|
26774
26890
|
const text = await readSkillFile(path);
|
|
26775
26891
|
if (text === void 0) continue;
|
|
26776
26892
|
const found = parseSkill(directory, text, path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftsamo/git-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Git tools for coding agents: state, commits, rewrites and stacked branches, worktrees and pushing.",
|
|
5
5
|
"author": "craftsamo",
|
|
6
6
|
"license": "LicenseRef-AGPL-3.0-Commons-Clause",
|
package/plugin.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: git-tools
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.2.0
|
|
3
3
|
description: "Git tools for coding agents: state, commits, rewrites and stacked branches, worktrees and pushing."
|
|
4
4
|
manifest_version: 1
|
|
5
5
|
requires_hermes: ">=0.22"
|
|
@@ -35,6 +35,9 @@ config_schema:
|
|
|
35
35
|
description: Single tools to remove by id. Wins over tools and toolsets.
|
|
36
36
|
language:
|
|
37
37
|
description: The language of the approval card, such as ja. Defaults to Hermes's display language, else English.
|
|
38
|
+
emoji:
|
|
39
|
+
type: dict
|
|
40
|
+
description: 'The emoji shown beside each toolset, by toolset name; "" shows none. Merged over the package''s emoji.'
|
|
38
41
|
published_text_rules:
|
|
39
42
|
description: The absolute path of a JSON file of published-text rules for the push tools. Unset, nothing is checked.
|
|
40
43
|
worktree_root:
|
|
@@ -55,8 +55,8 @@ below, and returns to the branch it started on.
|
|
|
55
55
|
replays; a commit whose change the base already has is dropped. `onto`
|
|
56
56
|
names another commit or ref.
|
|
57
57
|
- **Folding fixups in place** (`keep_base`): replays on the current base with
|
|
58
|
-
no fetch, folding `fixup
|
|
59
|
-
|
|
58
|
+
no fetch, folding each commit marked `fixup!` or `squash!` or `amend!`
|
|
59
|
+
into its target.
|
|
60
60
|
- **After a lower layer landed** (`after`): when a layer was merged by a
|
|
61
61
|
squash, its commits are not on the default branch as they are. `after`
|
|
62
62
|
names that layer: only the layers above it are replayed onto the new base,
|
|
@@ -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
|
)
|