genshin-mods 4.3.3 → 4.5.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/.claude-plugin/plugin.json +10 -2
- package/README.md +47 -37
- package/package.json +2 -2
- package/src/models/CommandRefusal.ts +3 -0
- package/src/models/ModName.ts +9 -0
- package/src/models/SinFigures.ts +4 -0
- package/src/models/SinName.ts +12 -0
- package/src/models/SinPosterior.ts +4 -0
- package/src/models/SinScore.ts +7 -0
- package/src/models/SinStatusKind.ts +8 -0
- package/src/models/SinsResult.ts +6 -0
- package/src/models/TaskCall.ts +1 -7
- package/src/models/TaskStatus.ts +6 -0
- package/src/models/UsageWindow.ts +1 -3
- package/src/register.ts +7 -3
- package/src/services/InitialState.ts +7 -2
- package/src/services/ModDescriptionMap.ts +10 -6
- package/src/services/band/getCommissionSummary.ts +2 -1
- package/src/services/band/registerBand.ts +51 -4
- package/src/services/commission/foldTaskCall.ts +2 -1
- package/src/services/constants.ts +3 -8
- package/src/services/delegation/checkIsLookupCommand.ts +2 -8
- package/src/services/delegation/stripLeadingSegments.ts +8 -0
- package/src/services/diskScan/getDenyReason.ts +7 -0
- package/src/services/diskScan/getDiskScanRefusal.ts +2 -78
- package/src/services/diskScan/getScanCommandRefusal.ts +8 -0
- package/src/services/diskScan/getScanRefusal.ts +21 -0
- package/src/services/diskScan/isRecursiveFlag.ts +4 -0
- package/src/services/diskScan/isRecursiveLongFlag.ts +3 -0
- package/src/services/diskScan/isRoot.ts +8 -0
- package/src/services/diskScan/normalizePath.ts +7 -0
- package/src/services/diskScan/readGrepArguments.ts +25 -0
- package/src/services/diskScan/registerDiskScan.ts +2 -1
- package/src/services/readHome.ts +7 -0
- package/src/services/registerLifecycle.ts +93 -25
- package/src/services/resin/RateLimitKindUsageWindowMap.ts +2 -10
- package/src/services/resin/formatResetsAt.ts +1 -1
- package/src/services/resin/getReserveSummary.ts +4 -4
- package/src/services/resin/getReserveWindow.ts +8 -13
- package/src/services/resin/reserveText.ts +4 -4
- package/src/services/shell/getDollarSubstitutionBodies.ts +25 -0
- package/src/services/shell/getShellRefusal.ts +4 -38
- package/src/services/shell/tokenize.ts +8 -0
- package/src/services/sins/SinColorMap.ts +12 -0
- package/src/services/sins/SinStatusTextMap.ts +10 -0
- package/src/services/sins/appendSinEvidence.ts +18 -0
- package/src/services/sins/constants.ts +33 -0
- package/src/services/sins/getBarCells.ts +8 -0
- package/src/services/sins/getPosteriorTrail.ts +21 -0
- package/src/services/sins/getScoreAndSpread.ts +8 -0
- package/src/services/sins/getSinQuestions.ts +28 -0
- package/src/services/sins/getSinScores.ts +17 -0
- package/src/services/sins/getTrend.ts +13 -0
- package/src/services/sins/parseSinsResult.ts +5 -0
- package/src/services/sins/pickTypeSafeKey.ts +4 -0
- package/src/services/sins/readIsLightTheme.ts +8 -0
- package/src/services/sins/readSinStatus.ts +20 -0
- package/src/services/sins/readTypeSafeKey.ts +10 -0
- package/src/services/sins/registerSins.ts +56 -0
- package/src/services/sins/sinsScript.ts +47 -0
- package/src/services/ward/getAddRefusal.ts +14 -0
- package/src/services/ward/getCheckoutPeer.ts +1 -3
- package/src/services/ward/getCheckoutRefusal.ts +6 -0
- package/src/services/ward/getCleanRefusal.ts +8 -0
- package/src/services/ward/getComparablePath.ts +2 -0
- package/src/services/ward/getGitRefusal.ts +32 -0
- package/src/services/ward/getResetRefusal.ts +19 -0
- package/src/services/ward/getRestoreRefusal.ts +10 -0
- package/src/services/ward/getStashRefusal.ts +2 -0
- package/src/services/ward/getSubcommand.ts +16 -0
- package/src/services/ward/getWholeIndexDenyReason.ts +9 -0
- package/src/services/ward/getWholeIndexRefusal.ts +1 -121
- package/src/services/ward/isFlag.ts +1 -0
- package/src/services/ward/isWholeCheckout.ts +5 -0
- package/src/services/ward/registerWholeIndex.ts +2 -1
- package/src/services/ward/resolveRecordsPath.ts +2 -1
- package/src/services/ward/splitArguments.ts +14 -0
- package/types/index.d.ts +11 -5
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "genshin-mods",
|
|
4
4
|
"displayName": "Genshin Mods",
|
|
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)
|
|
5
|
+
"description": "Six Claude Code mods, each switched by its own command: 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), ward (a question before editing a file another session just changed, and a refusal of a git command over the whole index while another session shares the checkout), and pulse (the machine watcher). Two guards run in every session without a command: a delegation guard that nudges a run of lookups toward a haiku agent, and a disk-scan guard that refuses a Bash scan from a root or home folder.",
|
|
6
6
|
"author": { "name": "Esposter", "url": "https://github.com/Esposter" },
|
|
7
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"],
|
|
11
|
-
"types": "./types/index.d.ts"
|
|
11
|
+
"types": "./types/index.d.ts",
|
|
12
|
+
"userConfig": {
|
|
13
|
+
"typesafe_key": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"title": "TypeSafe key",
|
|
16
|
+
"description": "A TypeSafe API key for sins: with it, each prompt's seven sins are scored by one typed decision and drawn in a pane. The same option as genshin-persona's, stored in the credential store, never in a settings file.",
|
|
17
|
+
"sensitive": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
12
20
|
}
|
package/README.md
CHANGED
|
@@ -5,72 +5,82 @@
|
|
|
5
5
|
[![NPM downloads][badge-npm-downloads]][url-npm]
|
|
6
6
|
[![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
|
|
7
7
|
|
|
8
|
-
A Claude Code plugin of mods
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
8
|
+
A Claude Code plugin of mods. Five mods draw one band above the prompt, in the session character's colour:
|
|
9
|
+
|
|
10
|
+
- waypoints: the next steps, one press away
|
|
11
|
+
- what the session is spending, with a one-press handoff
|
|
12
|
+
- recording mode
|
|
13
|
+
- a goal's task list and clock
|
|
14
|
+
- a question before an edit to a file that another session just changed
|
|
15
|
+
|
|
16
|
+
Three guards also run:
|
|
17
|
+
|
|
18
|
+
- a delegation guard that refuses any subagent not on haiku, and nudges a run of lookups toward a haiku agent
|
|
19
|
+
- a disk-scan guard that refuses a Bash scan from a root or a home folder
|
|
20
|
+
- a whole-index guard that refuses a git command over the whole index while another session shares the checkout
|
|
21
|
+
|
|
22
|
+
Design notes live in [the specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/genshin-mods).
|
|
19
23
|
|
|
20
24
|
## Table of Contents
|
|
21
25
|
|
|
22
26
|
- 🚀 [Getting Started](#getting-started)
|
|
23
|
-
-
|
|
27
|
+
- ⌨️ [Commands](#commands)
|
|
24
28
|
- ⚖️ [License](#license)
|
|
25
29
|
|
|
26
30
|
---
|
|
27
31
|
|
|
28
32
|
## <a name="getting-started">🚀 Getting Started</a>
|
|
29
33
|
|
|
30
|
-
This repository is a Claude Code plugin marketplace named `esposter
|
|
34
|
+
This repository is a Claude Code plugin marketplace named `esposter`. Add it, then install the plugin:
|
|
31
35
|
|
|
32
36
|
```bash
|
|
33
37
|
claude plugin marketplace add Esposter/Esposter
|
|
34
38
|
claude plugin install genshin-mods@esposter
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
`/sins` scores with [TypeSafe](https://typesafe.ai). It needs a key. Set the key with one of these:
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
- the plugin's `typesafe_key` option, the same option as [`genshin-persona`](https://github.com/Esposter/Esposter/tree/main/packages/genshin-persona)
|
|
44
|
+
- the `TYPESAFE_API_KEY` variable, when the option is empty
|
|
45
|
+
|
|
46
|
+
Set the option at install:
|
|
40
47
|
|
|
41
48
|
```bash
|
|
42
|
-
claude plugin
|
|
43
|
-
claude --plugin-dir packages/genshin-mods
|
|
49
|
+
claude plugin install genshin-mods@esposter --config typesafe_key=<key>
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
A clone of this repository needs neither command. Once you trust the repository, `.agents/settings.json` enables the plugin at project scope.
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
With [`genshin-persona`](https://github.com/Esposter/Esposter/tree/main/packages/genshin-persona) installed too, the band takes the session character's colour. Without it, the band uses the game's interface gold.
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
To develop the plugin from a checkout, run these commands in order. They disable the installed copy, so only one copy answers:
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
```bash
|
|
59
|
+
claude plugin disable genshin-mods@esposter
|
|
60
|
+
claude --plugin-dir packages/genshin-mods
|
|
61
|
+
```
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
| :------------------------ | :------------------------------------------------------------------------------------------------------------------------------ |
|
|
56
|
-
| `/waypoints [on \| off]` | Suggests the next steps after each answered turn, or stops |
|
|
57
|
-
| `/resin [on \| off]` | Shows the cache, context, limits and cost row and its warning toast |
|
|
58
|
-
| `/veil [on \| off]` | Turns recording mode on or off; off by default |
|
|
59
|
-
| `/commission [on \| off]` | Shows the goal meter while a turn works through a task list |
|
|
60
|
-
| `/pulse [on \| off]` | Keeps the machine watcher running, or stops |
|
|
61
|
-
| `/ward [on \| off]` | Asks before an edit to a file another session just changed, and refuses a whole-index git command while one shares the checkout |
|
|
63
|
+
## <a name="commands">⌨️ Commands</a>
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
Each mod has its own slash command. With no argument, the command flips the mod. The choice is kept across sessions:
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
| Command | What it does |
|
|
68
|
+
| :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
69
|
+
| `/commission [on \| off]` | Shows the goal meter while a turn works through a task list |
|
|
70
|
+
| `/pulse [on \| off]` | Keeps the machine watcher running, or stops |
|
|
71
|
+
| `/resin [on \| off]` | Shows the cache, context, limits and cost row and its warning toast |
|
|
72
|
+
| `/sins [on \| off]` | Scores each prompt's seven sins with TypeSafe, drawn as bars in a pane. Needs the `typesafe_key` option or `TYPESAFE_API_KEY` |
|
|
73
|
+
| `/veil [on \| off]` | Turns recording mode on or off; off by default |
|
|
74
|
+
| `/ward [on \| off]` | Asks before an edit to a file another session just changed, and refuses a whole-index git command while another session shares the checkout |
|
|
75
|
+
| `/waypoints [on \| off]` | Suggests the next steps after each answered turn, or stops |
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
Focus the band with a click or `ctrl+x tab`. Then press:
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
- `w`: warm
|
|
80
|
+
- `c`: compact
|
|
81
|
+
- `h`: hand off
|
|
82
|
+
- `1` to `3`: send a waypoint
|
|
83
|
+
- `g`: open or close the whole task list
|
|
74
84
|
|
|
75
85
|
## <a name="license">⚖️ License</a>
|
|
76
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genshin-mods",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
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",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"test": "vitest",
|
|
37
37
|
"validate": "claude plugin validate ."
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "e30c001f3b4b15693e75fb53092d62a97ade4d2b"
|
|
40
40
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// The refusal for a command's name, the arguments after it and the folder it runs in from the session's ("" for the
|
|
2
|
+
// Session's own), undefined when the command is not one the guard reads
|
|
3
|
+
export type CommandRefusal<T = string> = (name: string, args: string[], directory: string) => T | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export enum SinName {
|
|
2
|
+
Envy = "envy",
|
|
3
|
+
Gluttony = "gluttony",
|
|
4
|
+
Greed = "greed",
|
|
5
|
+
Lust = "lust",
|
|
6
|
+
Pride = "pride",
|
|
7
|
+
Sloth = "sloth",
|
|
8
|
+
Wrath = "wrath",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The seven sins, in the order the pane draws them. Each is one `score` question Jev answers about a prompt
|
|
12
|
+
export const SIN_NAMES: readonly SinName[] = Object.values(SinName);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// What the sins mod last found: its scores when the call answered, or why it could not score the last prompt
|
|
2
|
+
export enum SinStatusKind {
|
|
3
|
+
Answered = "answered",
|
|
4
|
+
Failed = "failed",
|
|
5
|
+
NodeTooOld = "node-too-old",
|
|
6
|
+
NoKey = "no-key",
|
|
7
|
+
Unchecked = "unchecked",
|
|
8
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// What the plugin's node script answers: the seven answers, or why none were recorded
|
|
2
|
+
import type { SinStatusKind } from "./SinStatusKind";
|
|
3
|
+
|
|
4
|
+
export type SinsResult =
|
|
5
|
+
| { answers: Record<string, { confidence: number; score: number }>; result: SinStatusKind.Answered }
|
|
6
|
+
| { reason?: string; result: SinStatusKind.Failed | SinStatusKind.NodeTooOld | SinStatusKind.NoKey };
|
package/src/models/TaskCall.ts
CHANGED
|
@@ -3,12 +3,6 @@ import type { TaskTool } from "./TaskTool";
|
|
|
3
3
|
|
|
4
4
|
// One task tool call with what its result says, in the shapes the engine's own tools declare
|
|
5
5
|
export type TaskCall =
|
|
6
|
-
| {
|
|
7
|
-
id: string;
|
|
8
|
-
// oxlint-disable-next-line literal-union/no-string-literal-union -- The engine's own TaskUpdate statuses
|
|
9
|
-
status?: "deleted" | CommissionTask["status"];
|
|
10
|
-
subject?: string;
|
|
11
|
-
tool: TaskTool.TaskUpdate;
|
|
12
|
-
}
|
|
6
|
+
| { id: string; status?: "deleted" | CommissionTask["status"]; subject?: string; tool: TaskTool.TaskUpdate }
|
|
13
7
|
| { id: string; subject: string; tool: TaskTool.TaskCreate }
|
|
14
8
|
| { todos: { content: string; status: CommissionTask["status"] }[]; tool: TaskTool.TodoWrite };
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
export interface UsageWindow {
|
|
2
2
|
// The window's length, which its reset time is counted back from
|
|
3
3
|
durationMs: number;
|
|
4
|
-
// The usage line the window's maintenance
|
|
4
|
+
// The usage line the window's maintenance line starts at
|
|
5
5
|
maintenancePercentage: number;
|
|
6
6
|
// The window's name as the reserve's note spells it
|
|
7
7
|
name: string;
|
|
8
|
-
// The usage line the window's wind-down starts at
|
|
9
|
-
windDownPercentage: number;
|
|
10
8
|
}
|
package/src/register.ts
CHANGED
|
@@ -6,17 +6,21 @@ import { registerDelegation } from "./services/delegation/registerDelegation";
|
|
|
6
6
|
import { registerDiskScan } from "./services/diskScan/registerDiskScan";
|
|
7
7
|
import { registerLifecycle } from "./services/registerLifecycle";
|
|
8
8
|
import { registerReserveGate } from "./services/resin/registerReserveGate";
|
|
9
|
+
import { registerSins } from "./services/sins/registerSins";
|
|
9
10
|
import { registerVeil } from "./services/veil/registerVeil";
|
|
10
11
|
import { registerWard } from "./services/ward/registerWard";
|
|
11
12
|
import { registerWholeIndex } from "./services/ward/registerWholeIndex";
|
|
12
13
|
|
|
13
|
-
export const register: Register = (on) => {
|
|
14
|
-
|
|
14
|
+
export const register: Register = (on, options) => {
|
|
15
|
+
// The plugin's typesafe_key option, empty when it is not set: the sins readers fall back to the TYPESAFE_API_KEY variable
|
|
16
|
+
const typeSafeKey = typeof options.typesafe_key === "string" ? options.typesafe_key : "";
|
|
17
|
+
registerBand(on, typeSafeKey);
|
|
15
18
|
registerCommission(on);
|
|
16
19
|
registerDelegation(on);
|
|
17
20
|
registerDiskScan(on);
|
|
18
|
-
registerLifecycle(on);
|
|
21
|
+
registerLifecycle(on, typeSafeKey);
|
|
19
22
|
registerReserveGate(on);
|
|
23
|
+
registerSins(on, typeSafeKey);
|
|
20
24
|
registerVeil(on);
|
|
21
25
|
registerWard(on);
|
|
22
26
|
registerWholeIndex(on);
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { PluginState } from "claude-code";
|
|
2
2
|
|
|
3
|
+
import { SinStatusKind } from "../models/SinStatusKind";
|
|
4
|
+
|
|
3
5
|
// Every state value's initial, read by each file's atoms: the engine traces a state reference only to an atom made in
|
|
4
6
|
// The file that uses it, so each file makes its own and this is the one place their initials are written
|
|
5
7
|
export const InitialState: PluginState["genshin-mods"] = {
|
|
6
8
|
commission: { goal: "", openedAt: 0, tasks: [] },
|
|
7
|
-
enabledMods: { commission: true, pulse: true, resin: true, veil: false, ward: true, waypoints: true },
|
|
9
|
+
enabledMods: { commission: true, pulse: true, resin: true, sins: false, veil: false, ward: true, waypoints: true },
|
|
8
10
|
isCommissionExpanded: false,
|
|
9
11
|
isHandingOff: false,
|
|
10
12
|
lastCacheRequestAt: 0,
|
|
11
13
|
lastPrompt: "",
|
|
12
14
|
lookupStreak: 0,
|
|
13
15
|
now: 0,
|
|
14
|
-
reserveWindow: { isAheadOfPace: false,
|
|
16
|
+
reserveWindow: { isAheadOfPace: false, name: "", percentage: 0, resetsAt: "" },
|
|
17
|
+
sinHistory: {},
|
|
18
|
+
sinSession: 0,
|
|
19
|
+
sinStatus: SinStatusKind.Unchecked,
|
|
15
20
|
waypoints: [],
|
|
16
21
|
};
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { EnabledMods } from "../../types";
|
|
2
2
|
|
|
3
|
+
import { ModName } from "../models/ModName";
|
|
4
|
+
|
|
3
5
|
// Each mod's command, `/<name> [on|off]`, as the menu describes it
|
|
4
6
|
export const ModDescriptionMap: Record<keyof EnabledMods, string> = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
[ModName.Commission]: "Show a goal's task list, how far it is and how long it has run.",
|
|
8
|
+
[ModName.Pulse]: "Keep the machine watcher running.",
|
|
9
|
+
[ModName.Resin]: "Show the cache, context, limits and cost, with warm, compact and handoff.",
|
|
10
|
+
[ModName.Sins]: "Score each prompt's seven sins with TypeSafe, drawn as bars in a pane. Needs a TypeSafe key.",
|
|
11
|
+
[ModName.Veil]: "Recording mode: show emails, amounts, phone numbers and secrets as placeholders.",
|
|
12
|
+
[ModName.Ward]:
|
|
13
|
+
"Ask before editing a file another session changed in the last half hour, and refuse a whole-index git command while one shares the checkout.",
|
|
14
|
+
[ModName.Waypoints]: "Suggest the next steps after each answer, one press each.",
|
|
11
15
|
};
|
|
12
16
|
|
|
13
17
|
export const ModNames = Object.keys(ModDescriptionMap) as (keyof EnabledMods)[];
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Commission } from "../../../types";
|
|
2
2
|
|
|
3
|
+
import { TaskStatus } from "../../models/TaskStatus";
|
|
3
4
|
import { MINUTE_FORMATTER } from "../constants";
|
|
4
5
|
|
|
5
6
|
const PERCENT_FORMATTER = new Intl.NumberFormat("en", { style: "percent" });
|
|
6
7
|
|
|
7
8
|
// `goal · 3 of 4 · 75% · 12m`, the clock counting whole minutes from the commission's opening
|
|
8
9
|
export const getCommissionSummary = ({ goal, openedAt, tasks }: Commission, now: number): string => {
|
|
9
|
-
const done = tasks.filter(({ status }) => status ===
|
|
10
|
+
const done = tasks.filter(({ status }) => status === TaskStatus.Completed).length;
|
|
10
11
|
const elapsedMinutes = Math.floor(
|
|
11
12
|
Temporal.Duration.from({ milliseconds: Math.max(0, now - openedAt) }).total("minutes"),
|
|
12
13
|
);
|
|
@@ -4,9 +4,16 @@ import { atom, read, update } from "claude-code";
|
|
|
4
4
|
|
|
5
5
|
import type { EnabledMods } from "../../../types";
|
|
6
6
|
|
|
7
|
+
import { TaskStatus } from "../../models/TaskStatus";
|
|
7
8
|
import { ACCENT_COLOR, HANDOFF_QUESTION, MAX_SHOWN_TASKS, WARM_QUESTION } from "../constants";
|
|
8
9
|
import { InitialState } from "../InitialState";
|
|
9
10
|
import { getResinFigures } from "../resin/getResinFigures";
|
|
11
|
+
import { SINS_PANE_ID } from "../sins/constants";
|
|
12
|
+
import { getSinScores } from "../sins/getSinScores";
|
|
13
|
+
import { readIsLightTheme } from "../sins/readIsLightTheme";
|
|
14
|
+
import { readSinStatus } from "../sins/readSinStatus";
|
|
15
|
+
import { SinColorMap } from "../sins/SinColorMap";
|
|
16
|
+
import { SinStatusTextMap } from "../sins/SinStatusTextMap";
|
|
10
17
|
import { getCommissionSummary } from "./getCommissionSummary";
|
|
11
18
|
|
|
12
19
|
// The plugin's one band, a row per mod with something to say. `$` is followed only into functions in the file that
|
|
@@ -23,6 +30,7 @@ const lastCacheRequestAtAtom = atom(
|
|
|
23
30
|
InitialState.lastCacheRequestAt,
|
|
24
31
|
);
|
|
25
32
|
const nowAtom = atom({ key: "now", plugin: "genshin-mods" } as const, InitialState.now);
|
|
33
|
+
const sinHistoryAtom = atom({ key: "sinHistory", plugin: "genshin-mods" } as const, InitialState.sinHistory);
|
|
26
34
|
const waypointsAtom = atom({ key: "waypoints", plugin: "genshin-mods" } as const, InitialState.waypoints);
|
|
27
35
|
|
|
28
36
|
// The engine's press slot takes no promise; every action here resolves each of its outcomes itself
|
|
@@ -31,7 +39,7 @@ const press = (action: () => Promise<unknown>) => () => {
|
|
|
31
39
|
action();
|
|
32
40
|
};
|
|
33
41
|
|
|
34
|
-
const StatusMarkMap = {
|
|
42
|
+
const StatusMarkMap = { [TaskStatus.Completed]: "✓", [TaskStatus.InProgress]: "▸", [TaskStatus.Pending]: "·" } as const;
|
|
35
43
|
|
|
36
44
|
// A fork re-sends the conversation's prefix, which renews the cache from the moment it is sent, and adds no row to the
|
|
37
45
|
// Transcript
|
|
@@ -125,7 +133,7 @@ const drawCommissionRow = async (
|
|
|
125
133
|
const isExpanded = await read($, isCommissionExpandedAtom);
|
|
126
134
|
const shownTasks = isExpanded
|
|
127
135
|
? commission.tasks.slice(0, MAX_SHOWN_TASKS)
|
|
128
|
-
: commission.tasks.filter(({ status }) => status ===
|
|
136
|
+
: commission.tasks.filter(({ status }) => status === TaskStatus.InProgress);
|
|
129
137
|
const { Box, Button, Text } = $.ui.resolve(e);
|
|
130
138
|
return Box({
|
|
131
139
|
children: [
|
|
@@ -146,7 +154,7 @@ const drawCommissionRow = async (
|
|
|
146
154
|
...shownTasks.map(({ status, subject }) =>
|
|
147
155
|
Text({
|
|
148
156
|
children: ` ${StatusMarkMap[status]} ${subject}`,
|
|
149
|
-
dimColor: status !==
|
|
157
|
+
dimColor: status !== TaskStatus.InProgress,
|
|
150
158
|
wrap: "truncate-end",
|
|
151
159
|
}),
|
|
152
160
|
),
|
|
@@ -189,9 +197,47 @@ const drawWaypointsRow = async (
|
|
|
189
197
|
});
|
|
190
198
|
};
|
|
191
199
|
|
|
200
|
+
// The sins as one line, each in its colour, with the pane's own button; the pane holds the bars and the trends
|
|
201
|
+
const drawSinsRow = async (
|
|
202
|
+
$: EngineInterface,
|
|
203
|
+
e: RenderInput<"AbovePrompt">,
|
|
204
|
+
enabledMods: EnabledMods,
|
|
205
|
+
accent: string,
|
|
206
|
+
typeSafeKey: string,
|
|
207
|
+
): Promise<RenderElement | undefined> => {
|
|
208
|
+
if (!enabledMods.sins) return undefined;
|
|
209
|
+
|
|
210
|
+
const scores = getSinScores(await read($, sinHistoryAtom));
|
|
211
|
+
const isLight = await readIsLightTheme($);
|
|
212
|
+
const summary = SinStatusTextMap[await readSinStatus($, typeSafeKey)];
|
|
213
|
+
const { Box, Button, Text } = $.ui.resolve(e);
|
|
214
|
+
return Box({
|
|
215
|
+
children: [
|
|
216
|
+
Text({ bold: true, children: "Sins ", color: accent }),
|
|
217
|
+
...(summary ? [Text({ children: `${summary} `, dimColor: true })] : []),
|
|
218
|
+
...(scores.length === 0 && !summary ? [Text({ children: "no prompt scored yet ", dimColor: true })] : []),
|
|
219
|
+
...scores.map(({ name, score }) =>
|
|
220
|
+
Text({
|
|
221
|
+
children: `${name} ${Math.round(score * 100)}% `,
|
|
222
|
+
color: SinColorMap[name][isLight ? "light" : "dark"],
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
Button({
|
|
226
|
+
hotkey: "p",
|
|
227
|
+
key: "sins-panel",
|
|
228
|
+
label: "Panel",
|
|
229
|
+
onPress: press(() => $.ui.open({ id: SINS_PANE_ID, title: "Seven sins" })),
|
|
230
|
+
plain: true,
|
|
231
|
+
}),
|
|
232
|
+
],
|
|
233
|
+
flexDirection: "row",
|
|
234
|
+
flexWrap: "wrap",
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
192
238
|
// The veil's marker first, so a recording never runs with it off unnoticed, and the engine's own band whenever no
|
|
193
239
|
// Mod has anything to say
|
|
194
|
-
export const registerBand = (on: On): void => {
|
|
240
|
+
export const registerBand = (on: On, typeSafeKey: string): void => {
|
|
195
241
|
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
|
|
196
242
|
if (e.props.hasSurvey) return next(e);
|
|
197
243
|
|
|
@@ -202,6 +248,7 @@ export const registerBand = (on: On): void => {
|
|
|
202
248
|
drawResinRow($, e, enabledMods, accent),
|
|
203
249
|
drawCommissionRow($, e, enabledMods, accent),
|
|
204
250
|
drawWaypointsRow($, e, enabledMods, accent),
|
|
251
|
+
drawSinsRow($, e, enabledMods, accent, typeSafeKey),
|
|
205
252
|
])
|
|
206
253
|
).filter((row) => row !== undefined);
|
|
207
254
|
if (rows.length === 0 && !enabledMods.veil) return next(e);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommissionTask } from "../../../types";
|
|
2
2
|
import type { TaskCall } from "../../models/TaskCall";
|
|
3
3
|
|
|
4
|
+
import { TaskStatus } from "../../models/TaskStatus";
|
|
4
5
|
import { TaskTool } from "../../models/TaskTool";
|
|
5
6
|
|
|
6
7
|
const DELETED_STATUS = "deleted";
|
|
@@ -8,7 +9,7 @@ const DELETED_STATUS = "deleted";
|
|
|
8
9
|
export const foldTaskCall = (tasks: CommissionTask[], call: TaskCall): CommissionTask[] => {
|
|
9
10
|
switch (call.tool) {
|
|
10
11
|
case TaskTool.TaskCreate:
|
|
11
|
-
return [...tasks, { id: call.id, status:
|
|
12
|
+
return [...tasks, { id: call.id, status: TaskStatus.Pending, subject: call.subject }];
|
|
12
13
|
case TaskTool.TaskUpdate: {
|
|
13
14
|
const { id, status, subject } = call;
|
|
14
15
|
if (status === DELETED_STATUS) return tasks.filter((task) => task.id !== id);
|
|
@@ -5,11 +5,9 @@ export const CACHE_LIFETIME_MS: number = Temporal.Duration.from({ hours: 1 }).to
|
|
|
5
5
|
export const CACHE_LOW_MS: number = Temporal.Duration.from({ minutes: 10 }).total("milliseconds");
|
|
6
6
|
export const CACHE_WARNING_MS: number = Temporal.Duration.from({ minutes: 5 }).total("milliseconds");
|
|
7
7
|
export const USAGE_WARNING_PERCENTAGE = 80;
|
|
8
|
-
// The usage
|
|
8
|
+
// The usage line each window's maintenance line starts at
|
|
9
9
|
export const FIVE_HOUR_MAINTENANCE_PERCENTAGE = 80;
|
|
10
|
-
export const FIVE_HOUR_WIND_DOWN_PERCENTAGE = 90;
|
|
11
10
|
export const WEEKLY_MAINTENANCE_PERCENTAGE = 80;
|
|
12
|
-
export const WEEKLY_WIND_DOWN_PERCENTAGE = 90;
|
|
13
11
|
// A window under this share is never read for its pace, since an early reading projects wildly
|
|
14
12
|
export const PACE_FLOOR_PERCENTAGE = 50;
|
|
15
13
|
// Each window's length, which its reset time is counted back from
|
|
@@ -42,12 +40,9 @@ export const VEIL_SECTION_ID = "genshin-mods-veil";
|
|
|
42
40
|
export const VEIL_SYSTEM_SECTION =
|
|
43
41
|
"Recording mode is on: the screen is being recorded or shared. Never write an email address, a phone number, a money amount, an API key, a token or a password in a reply; write a placeholder such as [email], [phone], [amount] or [secret] instead, even when asked for the value. Tool calls still use the real values.";
|
|
44
42
|
export const RESERVE_SECTION_ID = "genshin-mods-reserve";
|
|
45
|
-
// The
|
|
46
|
-
export const RESERVE_INSTRUCTION =
|
|
47
|
-
"Until it resets, start no new agent or workflow that writes code or designs; have each running one commit what builds and end on a handoff spec; commit and push the work in hand; write every open item down with what it takes to resume it cold (its paths, what is done, the calls made, the next step); clean up idle servers, shells and monitors; keep only long-running compute going.";
|
|
48
|
-
// The maintenance tier's section ends on this instruction: the window's rest goes on cleanup a haiku agent can do cold
|
|
43
|
+
// The reserve's section ends on this instruction: the window's rest goes on cleanup a haiku agent can do cold
|
|
49
44
|
export const MAINTENANCE_INSTRUCTION =
|
|
50
|
-
"Until it resets,
|
|
45
|
+
"Until it resets, spend the rest of the window on haiku agents for cleanup: fix the findings of a code review of this window's own recent commits, drain an open sweep ledger, bring the docs and skills in line with the code changed this window, fix CI's typecheck and lint reds, and land the refactors, simplifications and optimisations the compute queue's runs surfaced; write a finding that needs judgement down as a follow-up (the follow-ups plugin's capture skill) rather than deciding it on haiku; keep the compute queue's runners going.";
|
|
51
46
|
// The tools that look something up, the tools that change files, and the shells whose command decides which it is
|
|
52
47
|
export const LOOKUP_TOOLS: readonly string[] = ["Glob", "Grep", "Read", "WebFetch"];
|
|
53
48
|
export const RESET_TOOLS: readonly string[] = ["Agent", "Edit", "NotebookEdit", "Write"];
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// A session chains `export` and `cd` ahead of its real command, so each leading segment is stripped before the
|
|
4
|
-
// Command's first word is read
|
|
5
|
-
const stripLeadingSegments = (command: string): string => {
|
|
6
|
-
const rest = command.replace(LEADING_SEGMENT_REGEX, "");
|
|
7
|
-
return rest === command ? command : stripLeadingSegments(rest);
|
|
8
|
-
};
|
|
1
|
+
import { LOOKUP_COMMAND_REGEX, WRITING_LOOKUP_REGEX } from "../constants";
|
|
2
|
+
import { stripLeadingSegments } from "./stripLeadingSegments";
|
|
9
3
|
|
|
10
4
|
export const checkIsLookupCommand = (command: string): boolean => {
|
|
11
5
|
const rest = stripLeadingSegments(command);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LEADING_SEGMENT_REGEX } from "../constants";
|
|
2
|
+
|
|
3
|
+
// A session chains `export` and `cd` ahead of its real command, so each leading segment is stripped before the
|
|
4
|
+
// Command's first word is read
|
|
5
|
+
export const stripLeadingSegments = (command: string): string => {
|
|
6
|
+
const rest = command.replace(LEADING_SEGMENT_REGEX, "");
|
|
7
|
+
return rest === command ? command : stripLeadingSegments(rest);
|
|
8
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { KNOWN_HOME_LINES } from "./getDiskScanRefusal";
|
|
2
|
+
|
|
3
|
+
export const getDenyReason = (scanName: string): string =>
|
|
4
|
+
[
|
|
5
|
+
`Refused: ${scanName} scans from a root or a home folder, which reads the whole disk for a file whose home is known, and costs minutes of CPU and disk for every agent running. Look where the file lives instead:`,
|
|
6
|
+
...KNOWN_HOME_LINES,
|
|
7
|
+
].join("\n");
|