paseo-prompt-kit 0.5.2

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/client/actions/enabled.ts +30 -0
  4. package/client/commands/rewrite-command.ts +54 -0
  5. package/client/composer-bridge/adapter.ts +15 -0
  6. package/client/composer-bridge/dom.ts +101 -0
  7. package/client/composer-bridge/effect.ts +64 -0
  8. package/client/composer-bridge/fiber.ts +97 -0
  9. package/client/composer-bridge/web.ts +58 -0
  10. package/client/icon.ts +13 -0
  11. package/client/pills/agent-pills.ts +207 -0
  12. package/client/pills/rewrite-runner.ts +123 -0
  13. package/client/settings/action-samples.ts +102 -0
  14. package/client/settings/api-endpoints.ts +156 -0
  15. package/client/settings/custom-actions.ts +79 -0
  16. package/client/settings/draft.ts +82 -0
  17. package/client/settings/model-filter.ts +33 -0
  18. package/client/settings/read-settings.ts +45 -0
  19. package/client/settings/readiness.ts +84 -0
  20. package/client/settings/sections/actions-section.tsx +75 -0
  21. package/client/settings/sections/advanced-section.tsx +127 -0
  22. package/client/settings/sections/api-endpoint-section.tsx +388 -0
  23. package/client/settings/sections/custom-actions-section.tsx +163 -0
  24. package/client/settings/sections/dedicated-model-section.tsx +136 -0
  25. package/client/settings/sections/engine-section.tsx +101 -0
  26. package/client/settings/sections/provider-map-card.tsx +89 -0
  27. package/client/settings/sections/stored-key-rows.tsx +106 -0
  28. package/client/settings/selection.ts +46 -0
  29. package/client/settings/settings-saved.ts +17 -0
  30. package/client/settings/settings-screen.tsx +197 -0
  31. package/client/settings/ui/button.tsx +56 -0
  32. package/client/settings/ui/notice.tsx +61 -0
  33. package/client/settings/ui/split-select.tsx +26 -0
  34. package/client/settings/ui/status-bar.tsx +89 -0
  35. package/client/settings/ui/tokens.ts +38 -0
  36. package/client/settings/validation.ts +50 -0
  37. package/client/sheet/rewrite-sheet.tsx +249 -0
  38. package/index.client.tsx +71 -0
  39. package/index.server.ts +98 -0
  40. package/package.json +53 -0
  41. package/paseo-plugin.json +6 -0
  42. package/server/log.ts +20 -0
  43. package/server/model-resolver/provider-catalog.ts +37 -0
  44. package/server/model-resolver/resolver.ts +196 -0
  45. package/server/paseo-types.ts +13 -0
  46. package/server/rewrite-engine/engine.ts +88 -0
  47. package/server/rewrite-engine/handler.ts +94 -0
  48. package/server/rewrite-engine/output-validator.ts +130 -0
  49. package/server/transports/api/anthropic.ts +61 -0
  50. package/server/transports/api/cloudflare.ts +52 -0
  51. package/server/transports/api/gemini.ts +62 -0
  52. package/server/transports/api/key.ts +95 -0
  53. package/server/transports/api/openai.ts +52 -0
  54. package/server/transports/api/protocol.ts +96 -0
  55. package/server/transports/api/runner.ts +284 -0
  56. package/server/transports/api/secrets-store.ts +90 -0
  57. package/server/transports/cli/family.ts +216 -0
  58. package/server/transports/cli/process.ts +118 -0
  59. package/server/transports/cli/runner.ts +89 -0
  60. package/shared/action-registry/loader.ts +63 -0
  61. package/shared/action-registry/registry.ts +47 -0
  62. package/shared/action-registry/rewrite-contract.ts +31 -0
  63. package/shared/action-registry/schema.ts +65 -0
  64. package/shared/action-registry/wrapper.ts +30 -0
  65. package/shared/api-protocol.ts +56 -0
  66. package/shared/cli-families.ts +29 -0
  67. package/shared/language-registry/loader.ts +53 -0
  68. package/shared/language-registry/registry.ts +20 -0
  69. package/shared/language-registry/schema.ts +21 -0
  70. package/shared/languages/en.json +6 -0
  71. package/shared/languages/index.ts +5 -0
  72. package/shared/languages/vi.json +6 -0
  73. package/shared/packs/general.json +17 -0
  74. package/shared/packs/index.ts +12 -0
  75. package/shared/protected-literals.ts +550 -0
  76. package/shared/rpc.ts +187 -0
  77. package/shared/settings.ts +90 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Protected-literal rules for PromptKit rewrites.
3
+ *
4
+ * Each rule is data: a kind, a regular expression source, which capture group
5
+ * holds the literal, and an optional named shape gate. The extractor is pure so
6
+ * the same rules can be unit-tested and reused by the server validator.
7
+ *
8
+ * A rule whose pattern can also match plain prose carries a `shape`; the literal
9
+ * counts only when that pure predicate accepts it. Freezing prose is as harmful
10
+ * as dropping a literal: a rewrite that rephrases "write tests for login" must
11
+ * not fail because the word "Write" was read as a tool name.
12
+ */
13
+ export type ProtectedLiteralKind =
14
+ | "fenced_code"
15
+ | "inline_code"
16
+ | "url"
17
+ | "absolute_path"
18
+ | "relative_path"
19
+ | "filename"
20
+ | "shell_command"
21
+ | "identifier"
22
+ | "model_name"
23
+ | "tool_name";
24
+
25
+ export type RuleShape = "shell-command-line" | "model-family-name" | "tool-name";
26
+
27
+ export interface ProtectedLiteralRule {
28
+ kind: ProtectedLiteralKind;
29
+ source: string;
30
+ flags: string;
31
+ /** Capture group holding the literal; 0 means the whole match. */
32
+ capture: number;
33
+ /** Named pure validator applied to a match; a rejected match is not a literal. */
34
+ shape?: RuleShape;
35
+ /** Fixed alternatives for a rule whose vocabulary is a closed list. */
36
+ literals?: readonly string[];
37
+ description: string;
38
+ }
39
+
40
+ export interface ProtectedLiteral {
41
+ kind: ProtectedLiteralKind;
42
+ value: string;
43
+ start: number;
44
+ end: number;
45
+ }
46
+
47
+ const FILE_EXTENSIONS = [
48
+ "ts",
49
+ "tsx",
50
+ "js",
51
+ "jsx",
52
+ "mjs",
53
+ "cjs",
54
+ "json",
55
+ "md",
56
+ "py",
57
+ "rb",
58
+ "rs",
59
+ "go",
60
+ "java",
61
+ "kt",
62
+ "swift",
63
+ "sh",
64
+ "bash",
65
+ "zsh",
66
+ "yml",
67
+ "yaml",
68
+ "toml",
69
+ "ini",
70
+ "cfg",
71
+ "css",
72
+ "scss",
73
+ "html",
74
+ "xml",
75
+ "sql",
76
+ "txt",
77
+ "env",
78
+ "lock",
79
+ ] as const;
80
+
81
+ const SHELL_COMMANDS = [
82
+ "npm",
83
+ "pnpm",
84
+ "yarn",
85
+ "npx",
86
+ "bun",
87
+ "bunx",
88
+ "git",
89
+ "gh",
90
+ "docker",
91
+ "docker-compose",
92
+ "kubectl",
93
+ "helm",
94
+ "make",
95
+ "cargo",
96
+ "go",
97
+ "python",
98
+ "python3",
99
+ "pip",
100
+ "pip3",
101
+ "poetry",
102
+ "uv",
103
+ "node",
104
+ "deno",
105
+ "tsc",
106
+ "vitest",
107
+ "jest",
108
+ "pytest",
109
+ "ruff",
110
+ "eslint",
111
+ "prettier",
112
+ "biome",
113
+ "rg",
114
+ "grep",
115
+ "find",
116
+ "sed",
117
+ "awk",
118
+ "cat",
119
+ "ls",
120
+ "cd",
121
+ "mkdir",
122
+ "rmdir",
123
+ "rm",
124
+ "cp",
125
+ "mv",
126
+ "ln",
127
+ "chmod",
128
+ "chown",
129
+ "ssh",
130
+ "scp",
131
+ "rsync",
132
+ "tar",
133
+ "zip",
134
+ "unzip",
135
+ "curl",
136
+ "wget",
137
+ "brew",
138
+ "apt",
139
+ "apt-get",
140
+ "systemctl",
141
+ "psql",
142
+ "mysql",
143
+ "sqlite3",
144
+ "jq",
145
+ "echo",
146
+ "export",
147
+ "source",
148
+ ] as const;
149
+
150
+ /**
151
+ * Commands whose second word is drawn from a closed subcommand vocabulary.
152
+ * A bare `<command> <word>` is a command line only when that word is in the
153
+ * vocabulary: `git status` is a command, `make the login page work again` is not.
154
+ * Commands with open-ended operands (`make`, `cat`, `find`) are absent on purpose.
155
+ */
156
+ const COMMAND_SUBCOMMANDS: Readonly<Record<string, readonly string[]>> = {
157
+ git: [
158
+ "add", "am", "apply", "archive", "bisect", "blame", "branch", "bundle", "cat-file",
159
+ "check-ignore", "checkout", "cherry-pick", "clean", "clone", "commit", "config",
160
+ "describe", "diff", "fetch", "format-patch", "fsck", "grep", "help", "init", "log",
161
+ "ls-files", "ls-remote", "ls-tree", "merge", "mv", "notes", "pull", "push", "rebase",
162
+ "reflog", "remote", "reset", "restore", "revert", "rev-parse", "rm", "shortlog",
163
+ "show", "show-ref", "stash", "status", "submodule", "switch", "symbolic-ref", "tag",
164
+ "worktree",
165
+ ],
166
+ npm: [
167
+ "add", "audit", "cache", "ci", "config", "dedupe", "diff", "dist-tag", "doctor", "exec",
168
+ "explain", "fund", "help", "init", "install", "link", "login", "logout", "ls", "outdated",
169
+ "pack", "ping", "pkg", "prefix", "publish", "query", "rebuild", "repo", "restart", "root",
170
+ "run", "sbom", "search", "star", "stop", "team", "test", "token", "uninstall", "unpublish",
171
+ "update", "version", "view", "whoami",
172
+ ],
173
+ pnpm: [
174
+ "add", "audit", "build", "create", "dedupe", "deploy", "dlx", "doctor", "exec", "fetch",
175
+ "import", "init", "install", "licenses", "link", "list", "ls", "outdated", "pack", "patch",
176
+ "prune", "publish", "rebuild", "remove", "root", "run", "setup", "start", "store", "test",
177
+ "unlink", "update", "why",
178
+ ],
179
+ yarn: [
180
+ "add", "audit", "bin", "cache", "config", "create", "dedupe", "dlx", "exec", "explain",
181
+ "import", "info", "init", "install", "link", "list", "node", "pack", "patch", "plugin",
182
+ "policies", "publish", "rebuild", "remove", "run", "set", "stage", "test", "unlink", "up",
183
+ "upgrade", "version", "why", "workspaces",
184
+ ],
185
+ bun: ["add", "build", "create", "init", "install", "link", "pm", "remove", "run", "test", "update", "upgrade", "x"],
186
+ cargo: [
187
+ "add", "bench", "build", "check", "clean", "clippy", "doc", "fetch", "fix", "fmt", "help",
188
+ "init", "install", "locate-project", "login", "metadata", "new", "owner", "package",
189
+ "publish", "remove", "report", "run", "rustc", "rustdoc", "search", "test", "tree",
190
+ "uninstall", "update", "vendor",
191
+ ],
192
+ go: ["build", "clean", "doc", "env", "fix", "fmt", "generate", "get", "install", "list", "mod", "run", "test", "tool", "version", "vet", "work"],
193
+ deno: ["bench", "bundle", "cache", "check", "compile", "doc", "eval", "fmt", "info", "install", "lint", "repl", "run", "task", "test", "uninstall", "upgrade"],
194
+ docker: [
195
+ "attach", "build", "commit", "compose", "container", "cp", "create", "diff", "events",
196
+ "exec", "export", "history", "image", "images", "import", "info", "inspect", "kill", "load",
197
+ "login", "logout", "logs", "manifest", "network", "pause", "plugin", "port", "ps", "pull",
198
+ "push", "rename", "restart", "rm", "rmi", "run", "save", "search", "start", "stats", "stop",
199
+ "swarm", "system", "tag", "top", "unpause", "version", "volume", "wait",
200
+ ],
201
+ "docker-compose": [
202
+ "build", "config", "create", "down", "events", "exec", "images", "kill", "logs", "ls",
203
+ "pause", "port", "ps", "pull", "push", "restart", "rm", "run", "scale", "start", "stop",
204
+ "top", "unpause", "up", "version",
205
+ ],
206
+ kubectl: [
207
+ "annotate", "api-resources", "api-versions", "apply", "attach", "auth", "autoscale",
208
+ "certificate", "cluster-info", "completion", "config", "cordon", "cp", "create", "delete",
209
+ "describe", "diff", "drain", "edit", "exec", "explain", "expose", "get", "kustomize",
210
+ "label", "logs", "options", "patch", "plugin", "port-forward", "proxy", "replace",
211
+ "rollout", "run", "scale", "set", "taint", "top", "uncordon", "version", "wait",
212
+ ],
213
+ helm: [
214
+ "completion", "create", "dependency", "env", "get", "history", "install", "lint", "list",
215
+ "package", "plugin", "pull", "push", "registry", "repo", "rollback", "search", "show",
216
+ "status", "template", "test", "uninstall", "upgrade", "version",
217
+ ],
218
+ gh: [
219
+ "alias", "api", "auth", "browse", "cache", "codespace", "completion", "config", "extension",
220
+ "gist", "issue", "label", "org", "pr", "project", "release", "repo", "run", "search",
221
+ "secret", "ssh-key", "status", "variable", "workflow",
222
+ ],
223
+ brew: [
224
+ "cat", "cleanup", "config", "deps", "doctor", "info", "install", "leaves", "link", "list",
225
+ "outdated", "search", "services", "tap", "uninstall", "unlink", "untap", "update",
226
+ "upgrade", "uses",
227
+ ],
228
+ systemctl: [
229
+ "cat", "daemon-reload", "disable", "enable", "is-active", "is-enabled", "list-unit-files",
230
+ "list-units", "reload", "restart", "show", "start", "status", "stop",
231
+ ],
232
+ pip: ["cache", "check", "config", "download", "freeze", "install", "list", "search", "show", "uninstall", "wheel"],
233
+ pip3: ["cache", "check", "config", "download", "freeze", "install", "list", "search", "show", "uninstall", "wheel"],
234
+ uv: ["add", "build", "init", "lock", "pip", "publish", "python", "remove", "run", "sync", "tool", "tree", "venv"],
235
+ poetry: ["add", "build", "check", "config", "env", "export", "init", "install", "lock", "new", "publish", "remove", "run", "shell", "show", "update", "version"],
236
+ vitest: ["bench", "dev", "init", "list", "migrate", "related", "run", "typecheck", "watch"],
237
+ };
238
+
239
+ // `command` is deliberately absent: as a bare prefix it collides with the English
240
+ // compound "command-line", and no enabled daemon provider reports a `command-*` model.
241
+ const MODEL_NAME_PREFIXES = [
242
+ "claude",
243
+ "gpt",
244
+ "gemini",
245
+ "llama",
246
+ "mistral",
247
+ "mixtral",
248
+ "qwen",
249
+ "deepseek",
250
+ "grok",
251
+ "sonar",
252
+ ] as const;
253
+
254
+ const MODEL_FAMILY_NAMES = ["opus", "sonnet", "haiku", "codex"] as const;
255
+
256
+ const TOOL_NAMES = [
257
+ "Read",
258
+ "Write",
259
+ "Edit",
260
+ "MultiEdit",
261
+ "NotebookEdit",
262
+ "Bash",
263
+ "Glob",
264
+ "Grep",
265
+ "LS",
266
+ "Task",
267
+ "WebFetch",
268
+ "WebSearch",
269
+ "TodoWrite",
270
+ ] as const;
271
+
272
+ const TOOL_CONTEXT_WORDS = ["tool", "tools", "toolkit"] as const;
273
+
274
+ const FENCE_PATTERN = /```[^\n]*\n([\s\S]*?)```/g;
275
+ const INLINE_PATTERN = /`([^`\n]+)`/g;
276
+
277
+ /**
278
+ * Builds an alternation from a closed vocabulary. Each entry is regex-escaped so
279
+ * the function stays correct if a future entry carries a metacharacter; the
280
+ * current vocabularies are plain identifiers and extensions.
281
+ */
282
+ function alternate(alternatives: readonly string[]): string {
283
+ return [...alternatives]
284
+ .sort((left, right) => right.length - left.length)
285
+ .map((entry) => entry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
286
+ .join("|");
287
+ }
288
+
289
+ /** Ranges of the text that are code: fenced bodies and inline spans. */
290
+ function codeRanges(text: string): readonly (readonly [number, number])[] {
291
+ const ranges: [number, number][] = [];
292
+ for (const match of text.matchAll(FENCE_PATTERN)) {
293
+ const body = match[1] ?? "";
294
+ const start = match.index + match[0].indexOf(body);
295
+ ranges.push([start, start + body.length]);
296
+ }
297
+ for (const match of text.matchAll(INLINE_PATTERN)) {
298
+ const start = match.index + 1;
299
+ const end = start + (match[1]?.length ?? 0);
300
+ if (ranges.some(([from, to]) => start >= from && start < to)) continue;
301
+ ranges.push([start, end]);
302
+ }
303
+ return ranges;
304
+ }
305
+
306
+ function isCodeContext(
307
+ ranges: readonly (readonly [number, number])[],
308
+ start: number,
309
+ ): boolean {
310
+ return ranges.some(([from, to]) => start >= from && start < to);
311
+ }
312
+
313
+ function stripPunctuation(token: string): string {
314
+ return token.replace(/^["'([{]+/, "").replace(/["')\]}.,;:!?]+$/, "");
315
+ }
316
+
317
+ const FLAG_TOKEN = /^-{1,2}[A-Za-z][A-Za-z0-9-]*(=.*)?$/;
318
+ const ASSIGNMENT_TOKEN = /^[A-Za-z_][A-Za-z0-9_]*=/;
319
+ const PATH_TOKEN = /^[./~]/;
320
+ const FILENAME_TOKEN = new RegExp(`^[A-Za-z0-9_@-]+\\.(?:${alternate(FILE_EXTENSIONS)})$`);
321
+
322
+ /** A token that only a command line, not an English sentence, would carry. */
323
+ function isCommandShapedToken(token: string): boolean {
324
+ const candidate = stripPunctuation(token);
325
+ if (candidate === "") return false;
326
+ return (
327
+ FLAG_TOKEN.test(candidate) ||
328
+ ASSIGNMENT_TOKEN.test(candidate) ||
329
+ PATH_TOKEN.test(candidate) ||
330
+ FILENAME_TOKEN.test(candidate)
331
+ );
332
+ }
333
+
334
+ interface ShapeInput {
335
+ text: string;
336
+ matchText: string;
337
+ value: string;
338
+ start: number;
339
+ end: number;
340
+ }
341
+
342
+ const EXPLICIT_PROMPT_PREFIX = /^[ \t]*(?:\$|>)/;
343
+
344
+ function acceptsShellCommandLine(
345
+ input: ShapeInput,
346
+ ranges: readonly (readonly [number, number])[],
347
+ ): boolean {
348
+ if (EXPLICIT_PROMPT_PREFIX.test(input.matchText)) return true;
349
+ if (isCodeContext(ranges, input.start)) return true;
350
+ const tokens = input.value.trim().split(/\s+/).filter(Boolean);
351
+ const command = tokens[0];
352
+ if (command === undefined) return false;
353
+ const vocabulary = COMMAND_SUBCOMMANDS[command];
354
+ const second = stripPunctuation(tokens[1] ?? "");
355
+ if (vocabulary && second !== "" && vocabulary.includes(second)) return true;
356
+ return tokens.slice(1).some(isCommandShapedToken);
357
+ }
358
+
359
+ function acceptsModelFamilyName(
360
+ input: ShapeInput,
361
+ ranges: readonly (readonly [number, number])[],
362
+ ): boolean {
363
+ if (isCodeContext(ranges, input.start)) return true;
364
+ const lineStart = input.text.lastIndexOf("\n", input.start - 1) + 1;
365
+ const line = input.text.slice(lineStart, input.text.indexOf("\n", input.end) === -1
366
+ ? input.text.length
367
+ : input.text.indexOf("\n", input.end));
368
+ const neighbours = `${line.slice(0, input.start - lineStart)} ${line.slice(input.end - lineStart)}`;
369
+ return MODEL_NAME_PREFIXES.some((prefix) =>
370
+ new RegExp(`\\b${prefix}\\b`, "i").test(neighbours),
371
+ );
372
+ }
373
+
374
+ function acceptsToolName(
375
+ input: ShapeInput,
376
+ ranges: readonly (readonly [number, number])[],
377
+ ): boolean {
378
+ if (isCodeContext(ranges, input.start)) return true;
379
+ const lineStart = input.text.lastIndexOf("\n", input.start - 1) + 1;
380
+ const lineEnd = input.text.indexOf("\n", input.end);
381
+ const line = input.text.slice(lineStart, lineEnd === -1 ? input.text.length : lineEnd);
382
+ return TOOL_CONTEXT_WORDS.some((word) =>
383
+ new RegExp(`\\b${word}\\b`, "i").test(line),
384
+ );
385
+ }
386
+
387
+ const SHAPES: Readonly<
388
+ Record<RuleShape, (input: ShapeInput, ranges: readonly (readonly [number, number])[]) => boolean>
389
+ > = {
390
+ "shell-command-line": acceptsShellCommandLine,
391
+ "model-family-name": acceptsModelFamilyName,
392
+ "tool-name": acceptsToolName,
393
+ };
394
+
395
+ export const protectedLiteralRules: readonly ProtectedLiteralRule[] = [
396
+ {
397
+ kind: "fenced_code",
398
+ source: "```[^\\n]*\\n([\\s\\S]*?)```",
399
+ flags: "g",
400
+ capture: 1,
401
+ description: "Fenced code block body",
402
+ },
403
+ {
404
+ kind: "inline_code",
405
+ source: "`([^`\\n]+)`",
406
+ flags: "g",
407
+ capture: 1,
408
+ description: "Inline code span",
409
+ },
410
+ {
411
+ kind: "url",
412
+ source: "https?://[^\\s<>\"'`)\\]]*[A-Za-z0-9/#?&=_+-]",
413
+ flags: "g",
414
+ capture: 0,
415
+ description: "HTTP(S) URL without trailing prose punctuation",
416
+ },
417
+ {
418
+ kind: "absolute_path",
419
+ source:
420
+ "(?<![^\\s(\"'=])((?:[A-Za-z]:\\\\|/)[A-Za-z0-9._@+-]+(?:[\\\\/][A-Za-z0-9._@+-]+)*[A-Za-z0-9_@+-])",
421
+ flags: "gm",
422
+ capture: 1,
423
+ description: "Absolute filesystem path",
424
+ },
425
+ {
426
+ kind: "relative_path",
427
+ source:
428
+ "(?<![^\\s(\"'=])(\\.{1,2}/(?:[A-Za-z0-9._@+-]+/)*[A-Za-z0-9._@+-]*[A-Za-z0-9_@+-])",
429
+ flags: "gm",
430
+ capture: 1,
431
+ description: "Relative filesystem path",
432
+ },
433
+ {
434
+ kind: "filename",
435
+ source: `\\b[A-Za-z0-9_@-]+\\.(?:${alternate(FILE_EXTENSIONS)})\\b`,
436
+ flags: "g",
437
+ capture: 0,
438
+ description: "File name with a known extension",
439
+ },
440
+ {
441
+ kind: "shell_command",
442
+ source: `^[ \\t]*(?:\\$ |> )?((?:${alternate(SHELL_COMMANDS)})\\b[^\\n]*)`,
443
+ flags: "gm",
444
+ capture: 1,
445
+ shape: "shell-command-line",
446
+ description: "Shell command line with a command-like shape",
447
+ },
448
+ {
449
+ kind: "identifier",
450
+ source:
451
+ "\\b[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*\\b|\\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\\b|\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b",
452
+ flags: "g",
453
+ capture: 0,
454
+ description: "Program identifier (camelCase, snake_case, SCREAMING_SNAKE_CASE)",
455
+ },
456
+ {
457
+ kind: "model_name",
458
+ source: `\\b(?:${alternate(MODEL_NAME_PREFIXES)})[-.][A-Za-z0-9][A-Za-z0-9.-]*\\b`,
459
+ flags: "gi",
460
+ capture: 0,
461
+ description: "Model name carrying a known vendor prefix",
462
+ },
463
+ {
464
+ kind: "model_name",
465
+ source: `\\b(?:${alternate(MODEL_FAMILY_NAMES)})\\b`,
466
+ flags: "g",
467
+ capture: 0,
468
+ shape: "model-family-name",
469
+ description: "Bare model family name, only in code or beside a vendor prefix",
470
+ },
471
+ {
472
+ kind: "tool_name",
473
+ literals: TOOL_NAMES,
474
+ source: `\\b(?:${alternate(TOOL_NAMES)})\\b`,
475
+ flags: "g",
476
+ capture: 0,
477
+ shape: "tool-name",
478
+ description: "Agent tool name, only in code or beside the word tool",
479
+ },
480
+ ];
481
+
482
+ function rulePattern(rule: ProtectedLiteralRule): RegExp {
483
+ return new RegExp(rule.source, rule.flags);
484
+ }
485
+
486
+ function collectRule(
487
+ rule: ProtectedLiteralRule,
488
+ text: string,
489
+ ranges: readonly (readonly [number, number])[],
490
+ ): ProtectedLiteral[] {
491
+ const found: ProtectedLiteral[] = [];
492
+ const pattern = rulePattern(rule);
493
+ for (;;) {
494
+ const match = pattern.exec(text);
495
+ if (!match) break;
496
+ if (match[0].length === 0) {
497
+ pattern.lastIndex += 1;
498
+ continue;
499
+ }
500
+ const raw = match[rule.capture];
501
+ if (raw === undefined) continue;
502
+ const value = raw.trim();
503
+ if (value.length === 0) continue;
504
+ const start = match.index + match[0].indexOf(raw) + (raw.length - raw.trimStart().length);
505
+ if (rule.shape) {
506
+ const candidate: ShapeInput = {
507
+ text,
508
+ matchText: match[0],
509
+ value,
510
+ start,
511
+ end: start + value.length,
512
+ };
513
+ if (!SHAPES[rule.shape](candidate, ranges)) continue;
514
+ }
515
+ found.push({ kind: rule.kind, value, start, end: start + value.length });
516
+ }
517
+ return found;
518
+ }
519
+
520
+ /** Extracts every protected literal from `text`, deduplicated by kind and value. */
521
+ export function extractProtectedLiterals(text: string): ProtectedLiteral[] {
522
+ const found: ProtectedLiteral[] = [];
523
+ const seen = new Set<string>();
524
+ const ranges = codeRanges(text);
525
+ for (const rule of protectedLiteralRules) {
526
+ for (const literal of collectRule(rule, text, ranges)) {
527
+ const key = `${literal.kind}\u0000${literal.value}`;
528
+ if (seen.has(key)) continue;
529
+ seen.add(key);
530
+ found.push(literal);
531
+ }
532
+ }
533
+ return found.sort(
534
+ (left, right) =>
535
+ left.start - right.start ||
536
+ left.end - right.end ||
537
+ left.kind.localeCompare(right.kind) ||
538
+ left.value.localeCompare(right.value),
539
+ );
540
+ }
541
+
542
+ /** Literals present in `original` and absent from `rewritten`. */
543
+ export function findMissingProtectedLiterals(
544
+ original: string,
545
+ rewritten: string,
546
+ ): ProtectedLiteral[] {
547
+ return extractProtectedLiterals(original).filter(
548
+ (literal) => !rewritten.includes(literal.value),
549
+ );
550
+ }