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.
@@ -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://esposter.com/docs/infra/claude-interface/genshin-mods",
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://esposter.com/docs/infra/claude-interface/genshin-mods) to level up.
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.2",
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://esposter.com/docs/infra/claude-interface/genshin-mods",
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": "144e3f98570b015e3181bfbc6cb70d346ce826b5"
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
- LOOKUP_COMMAND_REGEX.test(stripLeadingSegments(command));
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
- // A flag group holding r or R: `-R`, `-rn`, `-lR`, `--recursive`
23
- const isRecursiveFlag = (token: string) => /^-[a-zA-Z]*[rR]/u.test(token) || token === "--recursive";
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
- // The ls flag recurses on the capital R alone, since its lowercase r sorts in reverse
26
- const isUpperRecursiveFlag = (token: string) => /^-[a-zA-Z]*R/u.test(token);
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) => isUpperRecursiveFlag(arg)) && args.some((arg) => isRoot(arg)) ? name : undefined;
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)) ? name : undefined;
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 { parseRecords } from "./parseRecords";
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 { parseRecords } from "./parseRecords";
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")) ?? "");