genshin-mods 4.1.2 → 4.3.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +3 -5
- package/src/services/constants.ts +5 -0
- package/src/services/delegation/checkIsLookupCommand.ts +6 -3
- package/src/services/diskScan/getDiskScanRefusal.ts +14 -6
- package/src/services/ward/readRecords.ts +9 -0
- package/src/services/ward/registerWard.ts +2 -12
- package/src/services/ward/registerWholeIndex.ts +2 -12
- package/src/services/ward/resolveRecordsPath.ts +7 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"displayName": "Genshin Mods",
|
|
5
5
|
"description": "Six Claude Code mods: five under one band above the prompt, waypoints (the next steps, one press each), resin (cache, context, limits and cost, with warm, compact and a one-click handoff), veil (recording mode), commission (a goal's task list and clock) and ward (a question before editing a file another session just changed), and a delegation guard that nudges a run of lookups toward a haiku agent.",
|
|
6
6
|
"author": { "name": "Esposter", "url": "https://github.com/Esposter" },
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/genshin-mods",
|
|
8
8
|
"repository": "https://github.com/Esposter/Esposter",
|
|
9
9
|
"license": "Apache-2.0",
|
|
10
10
|
"keywords": ["cache", "collision", "genshin", "handoff", "mods", "recording", "tasks"],
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ claude --plugin-dir packages/genshin-mods
|
|
|
45
45
|
|
|
46
46
|
## <a name="documentation">📖 Documentation</a>
|
|
47
47
|
|
|
48
|
-
We highly recommend you take a look at the [documentation](https://
|
|
48
|
+
We highly recommend you take a look at the [documentation](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/genshin-mods) to level up.
|
|
49
49
|
|
|
50
50
|
### Command reference
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genshin-mods",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "A Claude Code plugin of mods: five under one band above the prompt (the next steps, the cache and usage with a one-click handoff, recording mode, a goal's task list and a question before editing a file another session just changed) a delegation guard that nudges a run of lookups toward a haiku agent, a disk-scan guard that refuses a Bash scan from a root or a home folder, and a whole-index guard that refuses a git command over the whole index while another session shares the checkout.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"genshin",
|
|
9
9
|
"mods"
|
|
10
10
|
],
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/genshin-mods",
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/Esposter/Esposter/issues"
|
|
14
14
|
},
|
|
@@ -31,12 +31,10 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"format": "oxfmt",
|
|
35
|
-
"format:check": "oxfmt --check",
|
|
36
34
|
"lint": "eslint .",
|
|
37
35
|
"lint:fix": "eslint --fix .",
|
|
38
36
|
"test": "vitest",
|
|
39
37
|
"validate": "claude plugin validate ."
|
|
40
38
|
},
|
|
41
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "777a39636b789c13af33ed13370246ccf6dbaf31"
|
|
42
40
|
}
|
|
@@ -60,6 +60,11 @@ export const LEADING_SEGMENT_REGEX: RegExp = /^\s*(?:export\s[^&;]*&&|cd\s[^&;]*
|
|
|
60
60
|
// oxlint-disable-next-line typescript/no-inferrable-types -- `isolatedDeclarations` demands the annotation this pattern would otherwise infer
|
|
61
61
|
export const LOOKUP_COMMAND_REGEX: RegExp =
|
|
62
62
|
/^\s*(?:cat|sed|grep|rg|find|ls|head|tail|awk|wc|get-content|get-childitem|select-string|git\s+(?:show|log|diff)|gh\s+run\s+view)(?:\s|$)/iu;
|
|
63
|
+
// The reads that write as well: sed's in-place edit, find's actions that write, and a redirection into a file, the
|
|
64
|
+
// Stderr and stdout merges and the null device aside
|
|
65
|
+
// oxlint-disable-next-line typescript/no-inferrable-types -- `isolatedDeclarations` demands the annotation this pattern would otherwise infer
|
|
66
|
+
export const WRITING_LOOKUP_REGEX: RegExp =
|
|
67
|
+
/\bsed\b.*\s(?:-[a-zA-Z]*i|--in-place)|\bfind\b.*\s-(?:delete|exec|execdir|ok|okdir|fprint0?|fprintf|fls)\b|>(?!&|\s*\/dev\/null\b)/iu;
|
|
63
68
|
// Every third lookup in a row, the chain a haiku agent should answer as one bounded question
|
|
64
69
|
export const DELEGATION_NUDGE_EVERY = 3;
|
|
65
70
|
export const DELEGATION_NUDGE =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LEADING_SEGMENT_REGEX, LOOKUP_COMMAND_REGEX } from "../constants";
|
|
1
|
+
import { LEADING_SEGMENT_REGEX, LOOKUP_COMMAND_REGEX, WRITING_LOOKUP_REGEX } from "../constants";
|
|
2
2
|
|
|
3
3
|
// A session chains `export` and `cd` ahead of its real command, so each leading segment is stripped before the
|
|
4
4
|
// Command's first word is read
|
|
@@ -7,5 +7,8 @@ const stripLeadingSegments = (command: string): string => {
|
|
|
7
7
|
return rest === command ? command : stripLeadingSegments(rest);
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export const checkIsLookupCommand = (command: string): boolean =>
|
|
11
|
-
|
|
10
|
+
export const checkIsLookupCommand = (command: string): boolean => {
|
|
11
|
+
const rest = stripLeadingSegments(command);
|
|
12
|
+
|
|
13
|
+
return LOOKUP_COMMAND_REGEX.test(rest) && !WRITING_LOOKUP_REGEX.test(rest);
|
|
14
|
+
};
|
|
@@ -19,11 +19,15 @@ const normalizePath = (token: string) => {
|
|
|
19
19
|
|
|
20
20
|
const isRoot = (token: string) => ROOT_REGEX.test(normalizePath(token));
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
22
|
+
// The long form `--recursive`, which getopt also takes as any prefix of it from `--rec` on, since `--re` names two options
|
|
23
|
+
const isRecursiveLongFlag = (token: string) => token.startsWith("--rec") && "recursive".startsWith(token.slice(2));
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
|
|
25
|
+
// A flag group holding the scan's recursive letter, or the long form: grep takes r or R (`-R`, `-rn`, `-lR`), and ls takes
|
|
26
|
+
// The capital R alone, since its lowercase r sorts in reverse
|
|
27
|
+
const GREP_RECURSIVE_FLAG_REGEX = /^-[a-zA-Z]*[rR]/u;
|
|
28
|
+
const LS_RECURSIVE_FLAG_REGEX = /^-[a-zA-Z]*R/u;
|
|
29
|
+
const isRecursiveFlag = (token: string, shortFlagRegex: RegExp) =>
|
|
30
|
+
shortFlagRegex.test(token) || isRecursiveLongFlag(token);
|
|
27
31
|
|
|
28
32
|
// A flag that gives grep its pattern as the next argument, or as the rest of the same one
|
|
29
33
|
const SEPARATE_PATTERN_FLAG_REGEX = /^-[a-zA-Z]*[ef]$|^--(?:regexp|file)$/u;
|
|
@@ -55,9 +59,13 @@ const readGrepArguments = (args: string[]): { flags: string[]; paths: string[] }
|
|
|
55
59
|
const getScanRefusal = (name: string, args: string[]): string | undefined => {
|
|
56
60
|
if (name === "find" || name === "du") return args.some((arg) => isRoot(arg)) ? name : undefined;
|
|
57
61
|
if (name === "ls")
|
|
58
|
-
return args.some((arg) =>
|
|
62
|
+
return args.some((arg) => isRecursiveFlag(arg, LS_RECURSIVE_FLAG_REGEX)) && args.some((arg) => isRoot(arg))
|
|
63
|
+
? name
|
|
64
|
+
: undefined;
|
|
59
65
|
const { flags, paths } = readGrepArguments(args);
|
|
60
|
-
return flags.some((flag) => isRecursiveFlag(flag)) && paths.some((path) => isRoot(path))
|
|
66
|
+
return flags.some((flag) => isRecursiveFlag(flag, GREP_RECURSIVE_FLAG_REGEX)) && paths.some((path) => isRoot(path))
|
|
67
|
+
? name
|
|
68
|
+
: undefined;
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
const getScanCommandRefusal: CommandRefusal = (name, args) =>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EngineInterface } from "claude-code";
|
|
2
|
+
|
|
3
|
+
import type { WardRecord } from "../../../types";
|
|
4
|
+
|
|
5
|
+
import { parseRecords } from "./parseRecords";
|
|
6
|
+
|
|
7
|
+
// The records as the file holds them, none when it does not yet exist
|
|
8
|
+
export const readRecords = async ($: EngineInterface, path: string): Promise<Record<string, WardRecord>> =>
|
|
9
|
+
(await $.fs.exists(path)) ? parseRecords(await $.fs.read(path)) : {};
|
|
@@ -2,26 +2,16 @@ import type { EngineInterface, On, ToolCallResult } from "claude-code";
|
|
|
2
2
|
|
|
3
3
|
import { atom, read } from "claude-code";
|
|
4
4
|
|
|
5
|
-
import type { WardRecord } from "../../../types";
|
|
6
|
-
|
|
7
5
|
import { WardAnswer, WardAnswers } from "../../models/WardAnswer";
|
|
8
6
|
import { InitialState } from "../InitialState";
|
|
9
7
|
import { getCollidingRecord } from "./getCollidingRecord";
|
|
10
|
-
import { getRecordsPath } from "./getRecordsPath";
|
|
11
8
|
import { getRecordsWithEdit } from "./getRecordsWithEdit";
|
|
12
|
-
import {
|
|
9
|
+
import { readRecords } from "./readRecords";
|
|
10
|
+
import { resolveRecordsPath } from "./resolveRecordsPath";
|
|
13
11
|
|
|
14
12
|
const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
|
|
15
13
|
const AGE_FORMATTER = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
16
14
|
|
|
17
|
-
// The records file under the person's home folder, the home read from this file's own environment lookup
|
|
18
|
-
const resolveRecordsPath = async ($: EngineInterface): Promise<string> =>
|
|
19
|
-
getRecordsPath((await $.env.get("HOME")) ?? (await $.env.get("USERPROFILE")) ?? "");
|
|
20
|
-
|
|
21
|
-
// The records as the file holds them, none when it does not yet exist
|
|
22
|
-
const readRecords = async ($: EngineInterface, path: string): Promise<Record<string, WardRecord>> =>
|
|
23
|
-
(await $.fs.exists(path)) ? parseRecords(await $.fs.read(path)) : {};
|
|
24
|
-
|
|
25
15
|
// Before the edit: the question, when another session's record calls for one. After it: the edit recorded
|
|
26
16
|
const ward = async (
|
|
27
17
|
$: EngineInterface,
|
|
@@ -2,26 +2,16 @@ import type { EngineInterface, On, ToolCallResult } from "claude-code";
|
|
|
2
2
|
|
|
3
3
|
import { atom, read } from "claude-code";
|
|
4
4
|
|
|
5
|
-
import type { WardRecord } from "../../../types";
|
|
6
|
-
|
|
7
5
|
import { InitialState } from "../InitialState";
|
|
8
6
|
import { joinDirectory } from "../shell/joinDirectory";
|
|
9
7
|
import { getCheckoutPeer } from "./getCheckoutPeer";
|
|
10
8
|
import { getParentFolder } from "./getParentFolder";
|
|
11
|
-
import { getRecordsPath } from "./getRecordsPath";
|
|
12
9
|
import { getWholeIndexDenyReason, getWholeIndexRefusal } from "./getWholeIndexRefusal";
|
|
13
|
-
import {
|
|
10
|
+
import { readRecords } from "./readRecords";
|
|
11
|
+
import { resolveRecordsPath } from "./resolveRecordsPath";
|
|
14
12
|
|
|
15
13
|
const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
|
|
16
14
|
|
|
17
|
-
// The records file under the person's home folder, the home read from this file's own environment lookup
|
|
18
|
-
const resolveRecordsPath = async ($: EngineInterface): Promise<string> =>
|
|
19
|
-
getRecordsPath((await $.env.get("HOME")) ?? (await $.env.get("USERPROFILE")) ?? "");
|
|
20
|
-
|
|
21
|
-
// The records as the file holds them, none when it does not yet exist
|
|
22
|
-
const readRecords = async ($: EngineInterface, path: string): Promise<Record<string, WardRecord>> =>
|
|
23
|
-
(await $.fs.exists(path)) ? parseRecords(await $.fs.read(path)) : {};
|
|
24
|
-
|
|
25
15
|
// The nearest folder at or above `folder` holding a `.git`, a worktree's file included, undefined when none does
|
|
26
16
|
const findCheckoutRoot = async ($: EngineInterface, folder: string): Promise<string | undefined> => {
|
|
27
17
|
if (await $.fs.exists(`${folder}/.git`)) return folder;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EngineInterface } from "claude-code";
|
|
2
|
+
|
|
3
|
+
import { getRecordsPath } from "./getRecordsPath";
|
|
4
|
+
|
|
5
|
+
// The records file under the person's home folder, the home read from the engine's environment lookup
|
|
6
|
+
export const resolveRecordsPath = async ($: EngineInterface): Promise<string> =>
|
|
7
|
+
getRecordsPath((await $.env.get("HOME")) ?? (await $.env.get("USERPROFILE")) ?? "");
|