@kaydash9999/banker-plugins 0.20.2 → 0.20.5

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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +179 -1
  4. package/PRIVACY.md +13 -3
  5. package/README.md +25 -2
  6. package/bin/banker.js +84 -40
  7. package/bin/lib/telemetry-config.mjs +31 -16
  8. package/codex/manifest.json +1 -1
  9. package/codex/transform-matrix.md +1 -1
  10. package/commands/setup.md +23 -0
  11. package/hooks/progress.mjs +143 -38
  12. package/hooks/register.mjs +13 -3
  13. package/hooks/update-checkin.mjs +15 -7
  14. package/hooks/update-fetch.mjs +15 -9
  15. package/hooks/update-notify.mjs +2 -9
  16. package/package.json +1 -1
  17. package/skills/3d-intro-build/SKILL.md +11 -5
  18. package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
  19. package/skills/3d-intro-build/references/curate.mjs +48 -14
  20. package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
  21. package/skills/3d-intro-build/references/prompts.md +4 -4
  22. package/skills/3d-intro-build/references/serve.mjs +6 -1
  23. package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
  24. package/skills/3d-intro-build/references/video-pool.mjs +250 -135
  25. package/skills/3d-intro-setup/SKILL.md +41 -10
  26. package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
  27. package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
  28. package/skills/anti-drm/SKILL.md +6 -3
  29. package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
  30. package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
  31. package/skills/audit-mock/SKILL.md +1 -1
  32. package/skills/audit-security/SKILL.md +64 -2
  33. package/skills/deep-research/SKILL.md +5 -0
  34. package/skills/docs-setup/SKILL.md +37 -7
  35. package/skills/lineage/SKILL.md +12 -12
  36. package/skills/lineage/lineage.py +399 -300
  37. package/skills/motion-graphic-make/SKILL.md +1 -1
  38. package/skills/motion-graphic-make/references/curate.mjs +48 -14
  39. package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
  40. package/skills/motion-graphic-make/references/serve.mjs +6 -1
  41. package/skills/motion-graphic-setup/SKILL.md +58 -13
  42. package/skills/obsidizer/SKILL.md +1 -1
  43. package/skills/obsidizer/obsidize.mjs +8 -1
  44. package/skills/omc-patch/SKILL.md +80 -23
  45. package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
  46. package/skills/payload-mon/SKILL.md +4 -2
  47. package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
  48. package/skills/payload-mon/scripts/payload-size.mjs +4 -1
  49. package/skills/pdf-vision-extract/SKILL.md +11 -6
  50. package/skills/ralph-qa/SKILL.md +125 -151
  51. package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
  52. package/skills/ralph-qa/references/payload-scan.mjs +22 -7
  53. package/skills/ralph-qa/references/seat-payload.mjs +646 -0
  54. package/skills/ralph-qa/references/seat-run.mjs +956 -0
  55. package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
  56. package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
  57. package/skills/setup-harness-factory/SKILL.md +17 -1
  58. package/skills/setup-insane-search/SKILL.md +16 -0
  59. package/skills/setup-java/SKILL.md +20 -1
  60. package/skills/setup-lsp/SKILL.md +56 -13
  61. package/skills/setup-mcp/SKILL.md +73 -4
  62. package/skills/setup-node/SKILL.md +31 -6
  63. package/skills/setup-omc/SKILL.md +24 -0
  64. package/skills/setup-omc-hud/SKILL.md +9 -6
  65. package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
  66. package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
  67. package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
  68. package/skills/setup-playwright/SKILL.md +45 -2
  69. package/skills/setup-pwsh/SKILL.md +19 -0
  70. package/skills/setup-python/SKILL.md +45 -7
  71. package/skills/setup-sandbox/SKILL.md +30 -5
  72. package/skills/setup-stitch/SKILL.md +22 -0
  73. package/skills/setup-tmux/SKILL.md +24 -4
  74. package/skills/tone-compact/SKILL.md +2 -1
  75. package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
  76. package/skills/ultra-chain/SKILL.md +4 -3
  77. package/skills/ultra-chain/references/preview-lib.mjs +51 -15
  78. package/skills/ultra-chain/references/serve.mjs +6 -1
  79. package/skills/ultra-interview/SKILL.md +1 -0
  80. package/skills/update-banker/SKILL.md +19 -3
  81. package/skills/vertical-pptx/SKILL.md +6 -3
  82. package/skills/vertical-pptx-setup/SKILL.md +76 -20
@@ -109,12 +109,18 @@ function projectRoot(dir) {
109
109
  }
110
110
 
111
111
  // <g>/projects.json, gemini's map from a project folder to its folder name under tmp/ and
112
- // history/, read the other way round: folder name to project folder.
112
+ // history/, read the other way round: folder name to the project folders that claim it.
113
113
  function registered(g) {
114
114
  try {
115
115
  const { projects } = JSON.parse(readFileSync(join(g, "projects.json"), "utf8"));
116
116
  if (!projects || typeof projects !== "object" || Array.isArray(projects)) return new Map();
117
- return new Map(Object.entries(projects).filter(([, name]) => typeof name === "string").map(([owner, name]) => [name, owner]));
117
+ const owners = new Map();
118
+ for (const [owner, name] of Object.entries(projects)) {
119
+ if (typeof name !== "string") continue;
120
+ if (!owners.has(name)) owners.set(name, new Set());
121
+ owners.get(name).add(resolve(owner));
122
+ }
123
+ return owners;
118
124
  } catch {
119
125
  return new Map();
120
126
  }
@@ -122,8 +128,10 @@ function registered(g) {
122
128
 
123
129
  // Each folder gemini keeps under tmp/ or history/, here and under .cache/.gemini, with the
124
130
  // folder its .project_root names. A folder without the marker (a seat that wrote its session
125
- // again after its run cleaned up) gets the project projects.json gives its name. Only folders
126
- // read from tmp/ and history/ are looked up, so no name there reaches another folder.
131
+ // again after its run cleaned up) gets the project projects.json gives its name. A name two
132
+ // project folders claim (a hand edit, another gemini) is no seat's for sure: its unmarked folder
133
+ // gets no owner, only the `claimants`, and stays. One folder written twice ("x", "x/") is one owner.
134
+ // Only folders read from tmp/ and history/ are looked up, so no name there reaches another folder.
127
135
  function projectFolders(env) {
128
136
  const home = geminiHome(env);
129
137
  return [join(home, ".gemini"), join(home, ".cache", ".gemini")].flatMap((g) => {
@@ -131,29 +139,50 @@ function projectFolders(env) {
131
139
  return ["tmp", "history"].flatMap((kind) => listDir(join(g, kind)).map((name) => {
132
140
  const dir = join(g, kind, name);
133
141
  const root = projectRoot(dir);
134
- return { dir, owner: root === undefined ? names.get(name) : root };
142
+ if (root !== undefined) return { dir, owner: root, claimants: [] };
143
+ const claimants = [...(names.get(name) ?? [])];
144
+ return { dir, owner: claimants.length === 1 ? claimants[0] : null, claimants };
135
145
  }));
136
- }).filter((p) => p.owner);
146
+ }).filter((p) => p.owner || p.claimants.length > 1);
137
147
  }
138
148
 
139
- // The folders gemini keeps for sessions run in `seat`, matched by the path as written and as
140
- // resolved; the seat folder may be gone already.
141
- export function sessionFolders(seat, { env = process.env, platform = process.platform } = {}) {
149
+ // Whether a project folder is `seat`, by the path as written or as resolved (it may be gone).
150
+ function seatOwner(seat, platform) {
142
151
  const want = new Set([norm(seat, platform), norm(real(seat), platform)]);
143
- return projectFolders(env).filter((p) => want.has(norm(p.owner, platform))).map((p) => p.dir);
152
+ return (owner) => want.has(norm(owner, platform));
144
153
  }
145
154
 
146
- // The session folders of seat folders under `base` that are gone: .project_root names
147
- // <base>/ralph-qa.<id>/gemini and that folder no longer exists (a run cut short before its clean).
148
- // A run still going has its seat folder, so its records stay.
155
+ // Whether a project folder is <base>/ralph-qa.<id>/gemini and gone (a run cut short before its
156
+ // clean). A run still going has its seat folder, so its records stay.
149
157
  const SEAT_PARENT = /^ralph-qa\.[A-Za-z0-9]+$/;
150
- export function staleSessionFolders(base, { env = process.env, platform = process.platform, exists = existsSync } = {}) {
158
+ function staleOwner(base, platform, exists) {
151
159
  const roots = new Set([norm(base, platform), norm(real(base), platform)]);
152
- const stale = ({ owner }) => {
160
+ return (owner) => {
153
161
  const o = norm(owner, platform);
154
162
  return basename(o) === "gemini" && SEAT_PARENT.test(basename(dirname(o))) && roots.has(dirname(dirname(o))) && !exists(owner);
155
163
  };
156
- return projectFolders(env).filter(stale).map((p) => p.dir);
164
+ }
165
+
166
+ // The folders to remove, of the project folders `mine` takes, and those left because their name
167
+ // is also another project folder's.
168
+ function folderPlan(env, mine) {
169
+ const all = projectFolders(env);
170
+ return {
171
+ dirs: all.filter((p) => p.owner && mine(p.owner)).map((p) => p.dir),
172
+ skipped: all.filter((p) => !p.owner && p.claimants.some(mine)).map((p) => p.dir),
173
+ };
174
+ }
175
+
176
+ // The folders gemini keeps for sessions run in `seat`, matched by the path as written and as
177
+ // resolved; the seat folder may be gone already.
178
+ export function sessionFolders(seat, { env = process.env, platform = process.platform } = {}) {
179
+ return folderPlan(env, seatOwner(seat, platform)).dirs;
180
+ }
181
+
182
+ // The session folders of seat folders under `base` that are gone: .project_root names
183
+ // <base>/ralph-qa.<id>/gemini and that folder no longer exists.
184
+ export function staleSessionFolders(base, { env = process.env, platform = process.platform, exists = existsSync } = {}) {
185
+ return folderPlan(env, staleOwner(base, platform, exists)).dirs;
157
186
  }
158
187
 
159
188
  function check(seat) {
@@ -176,9 +205,27 @@ function check(seat) {
176
205
  return 0;
177
206
  }
178
207
 
179
- function remove(dirs) {
208
+ // Removes `dirs`; gives back what it removed and the folders it left because their name is another project's too.
209
+ function removeFolders({ dirs, skipped }) {
180
210
  for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
181
- process.stdout.write(JSON.stringify({ removed: dirs }) + "\n");
211
+ return { removed: dirs, skipped };
212
+ }
213
+
214
+ // clean and sweep for a caller in the same process (the ralph-qa runner, seat-run.mjs).
215
+ export function cleanSeat(seat, { env = process.env, platform = process.platform } = {}) {
216
+ return removeFolders(folderPlan(env, seatOwner(seat, platform)));
217
+ }
218
+ export function sweepBase(base, { env = process.env, platform = process.platform, exists = existsSync } = {}) {
219
+ return removeFolders(folderPlan(env, staleOwner(base, platform, exists)));
220
+ }
221
+
222
+ // Removes `dirs` and says which folders it left (`skipped`), on stdout as JSON and one line each on stderr.
223
+ function remove(plan) {
224
+ const { removed, skipped } = removeFolders(plan);
225
+ for (const dir of skipped) {
226
+ process.stderr.write(`gemini-seat: left ${dir}: two project folders in projects.json claim its name, so it may hold another project's sessions; remove it by hand if they are the seat's\n`);
227
+ }
228
+ process.stdout.write(JSON.stringify({ removed, skipped }) + "\n");
182
229
  return 0;
183
230
  }
184
231
 
@@ -193,7 +240,8 @@ function main(argv) {
193
240
  return 2;
194
241
  }
195
242
  if (cmd === "check") return check(folder);
196
- return remove(cmd === "clean" ? sessionFolders(folder) : staleSessionFolders(folder));
243
+ const mine = cmd === "clean" ? seatOwner(folder, process.platform) : staleOwner(folder, process.platform, existsSync);
244
+ return remove(folderPlan(process.env, mine));
197
245
  }
198
246
 
199
247
  const invokedDirectly = () => {
@@ -10,12 +10,15 @@
10
10
  * none): the whole block is the secret, not just the BEGIN line. The matched text itself is never
11
11
  * printed, so the report cannot become the leak. The patterns cover common key and token
12
12
  * formats, private key blocks (PGP too), secret assignments (quoted, or unquoted with letters
13
- * and digits), passwords (quoted, with a symbol, alone on a config or env-file line, or after a
14
- * Dockerfile `ENV KEY value`), secrets given as command-line flags, curl's user and password flag,
13
+ * and digits), passwords (quoted, with a symbol, alone on a config or env-file line, a KEY=value
14
+ * word after a space, a quote, `(` or `=` as in `env`, `docker -e`, `--from-literal=`, or after a Dockerfile
15
+ * `ENV KEY value`; the shell's own PWD or OLDPWD holding a path is no password),
16
+ * secrets given as command-line flags, curl's user and password flag,
15
17
  * hex keys, Basic and Bearer credentials, credentials in URLs and
16
18
  * long high-entropy strings (a lockfile's sha512- integrity hash and a Go or Terraform h1: hash
17
19
  * aside); they do not prove a payload clean. A Docker secret file's path after a *_FILE key is
18
- * not read as the password it points to. Every repeat and lookahead is bounded (a run longer than
20
+ * not read as the password it points to, and a value masked as a whole `[REDACTED]` word (the mask
21
+ * SKILL.md asks for) is no value, so a masked payload scans clean; a mask glued to the rest of a value is not. Every repeat and lookahead is bounded (a run longer than
19
22
  * 65536 characters is read in pieces) or anchored, so a long line takes linear time and cannot
20
23
  * overflow the regex stack.
21
24
  */
@@ -25,6 +28,9 @@ import { fileURLToPath } from "node:url";
25
28
  const SECRET_KEY = String.raw`(?:api[_-]?key|secret|token|passw(?:or)?d|pwd|access[_-]?key|credential|비밀번호|패스워드)[\w-]{0,64}["']?\s*[:=]\s*`;
26
29
  // `pass` and `passphrase` count only as a word of their own (DB_PASS, "pass"), not inside bypass.
27
30
  const PASS_WORD = String.raw`(?:passw(?:or)?d|pwd|passcode|(?<![a-z])pass(?:phrase)?(?![a-z]))`;
31
+ // An env-style key that holds a password word, up to its `=`, and a value that is no reference or whole placeholder
32
+ // (`$X`, `<your-password>`, `{{ x }}`, `%(x)s`, `%X%`). A value that only starts with `<` or `{` is still read.
33
+ const ENV_KEY = String.raw`[A-Za-z0-9_]{0,64}(?:passw(?:or)?d|pwd|passcode)[A-Za-z0-9_]{0,64}=(?!-|\$|<[^>\s]{1,64}>|\{\{|%\(|%[A-Za-z_][A-Za-z0-9_]{0,64}%)`;
28
34
  const PASSWORD_KEY = String.raw`(?:${PASS_WORD}|비밀번호|패스워드)[\w-]{0,64}["']?\s*[:=]\s*`;
29
35
  // An unquoted value that is not code: none of the characters an expression or a reference
30
36
  // holds, and at least one symbol (an identifier has none).
@@ -54,7 +60,9 @@ export const PATTERNS = [
54
60
  ["assigned-secret", new RegExp(SECRET_KEY + String.raw`["'](?:[^"'\s]{12,256}|(?=[^"'\s]{0,10}\d)[^"'\s]{8,11})["']`, "i")],
55
61
  ["assigned-secret-bare", new RegExp(SECRET_KEY + String.raw`(?=[^\s"']{0,256}\d)(?=[^\s"']{0,256}[A-Za-z])[^\s"'(\[{]{12,256}`, "i")],
56
62
  ["assigned-password", new RegExp(PASSWORD_KEY + String.raw`(?:["'][^"'\s]{6,256}["']|(?=${BARE}{0,256}[^\w\s;,"'().\[\]{}$<>])${BARE}{6,256})`, "i")],
57
- ["env-password", /^[+\- ]?\s*(?:export\s+)?[A-Za-z0-9_]{0,64}(?:passw(?:or)?d|pwd|passcode)[A-Za-z0-9_]{0,64}=[^\s$"'][^\s"']{5,256}/i],
63
+ // at the head of a line, or as a word after a space, a quote, `(` or `=` (env, docker -e, --from-literal=, a subshell),
64
+ // its value ending the word
65
+ ["env-password", new RegExp(String.raw`^[+\- ]?\s*(?:export\s+)?${ENV_KEY}[^\s"']{6,256}|(?<=[\s"'(=])${ENV_KEY}[^\s"',;()\[\]{}]{6,256}(?=[\s"']|$)`, "i")],
58
66
  // a password alone on a config line: YAML, compose lists, .properties, Dockerfile ENV, diff marks
59
67
  ["config-password", new RegExp(String.raw`^[+\- ]?\s*(?:-\s+|export\s+|ENV\s+)?[\w.-]{0,64}${PASS_WORD}[\w.-]{0,64}["']?\s*[:=]\s*["']?(?![-$<{%])[^\s"',;()\[\]{}]{6,256}["']?\s*(?:#.*)?$`, "i")],
60
68
  // a Dockerfile's legacy `ENV KEY value` form, the value alone after the space
@@ -94,10 +102,16 @@ const highEntropy = (line) => [...line.matchAll(LONG_RUN)].some((m) => !publicHa
94
102
  // not that password: the kinds that read a value after a key name see a `-` there instead. The
95
103
  // other kinds still read the line as it is, so a key's own format shows wherever it sits.
96
104
  const SECRET_FILE_PATH = /(?<![\w.-])([\w.-]{0,128}_FILE["']?(?:[ \t]*[:=][ \t]*|[ \t]+)["']?)\/[\w.@-]{1,128}(?:\/[\w.@-]{1,128}){0,32}(?=["']?(?:[\s,;#]|$))/gi;
105
+ // The shell's own working folder (PWD, OLDPWD, as `env`, `declare -x` or a JSON dump shows it) holding a plain path:
106
+ // no password, though PWD reads like one. A key that only ends in PWD (MYSQL_PWD) and a value that is no plain path
107
+ // stay. Taken out before the keyed patterns read the line, as lineage does.
108
+ const SHELL_DIR = /(?<=^[+\- ]?|[\s"'$])((?:OLD)?PWD["']?[ \t]*[:=][ \t]*["']?)(?:~|\/|[A-Za-z]:[\\/])[^\s"'!#$%&*;|<>()[\]{}`^?=]{0,4096}(?=["']?(?:[\s,]|$))/g;
97
109
  const KEYED = new Set(["assigned-secret", "assigned-secret-bare", "assigned-password", "env-password", "config-password",
98
110
  "dockerfile-env-password"]);
99
111
 
100
112
  const KEY_END = new RegExp(`-----END ${PRIVATE_KEY}`);
113
+ // The mask SKILL.md asks for, as a word of its own. One glued to the rest of a value is no mask: what is left is read.
114
+ const WHOLE_MASK = /(?<!\w)\[REDACTED\](?!\w)/g;
101
115
 
102
116
  // Each line's secret-like kinds, once per kind and line. A private key closes at the first END
103
117
  // marker at or after its line: the END lines are found once and walked with the lines, so many
@@ -109,12 +123,13 @@ export function scan(text) {
109
123
  let next = 0;
110
124
  lines.forEach((line, i) => {
111
125
  while (next < ends.length && ends[next] < i + 1) next += 1;
112
- const keyed = line.replace(SECRET_FILE_PATH, "$1-");
126
+ const plain = line.replace(WHOLE_MASK, "-");
127
+ const keyed = plain.replace(SECRET_FILE_PATH, "$1-").replace(SHELL_DIR, "$1-");
113
128
  for (const [kind, re] of PATTERNS) {
114
- if (!re.test(KEYED.has(kind) ? keyed : line)) continue;
129
+ if (!re.test(KEYED.has(kind) ? keyed : plain)) continue;
115
130
  hits.push(kind === "private-key" ? { line: i + 1, kind, end: ends[next] ?? null } : { line: i + 1, kind });
116
131
  }
117
- if (highEntropy(line)) hits.push({ line: i + 1, kind: "high-entropy" });
132
+ if (highEntropy(plain)) hits.push({ line: i + 1, kind: "high-entropy" });
118
133
  });
119
134
  return { count: hits.length, hits };
120
135
  }