@mrclrchtr/supi-lsp 0.1.0 → 1.1.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 (72) hide show
  1. package/README.md +112 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +26 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/package.json +16 -11
  19. package/{capabilities.ts → src/capabilities.ts} +8 -0
  20. package/src/client/client-refresh.ts +229 -0
  21. package/{client.ts → src/client/client.ts} +178 -30
  22. package/{transport.ts → src/client/transport.ts} +10 -6
  23. package/src/config.ts +143 -0
  24. package/src/defaults.json +82 -0
  25. package/src/diagnostics/diagnostic-augmentation.ts +82 -0
  26. package/src/diagnostics/diagnostic-display.ts +68 -0
  27. package/{diagnostic-summary.ts → src/diagnostics/diagnostic-summary.ts} +11 -7
  28. package/{diagnostics.ts → src/diagnostics/diagnostics.ts} +9 -4
  29. package/src/diagnostics/stale-diagnostics.ts +47 -0
  30. package/src/diagnostics/suppression-diagnostics.ts +58 -0
  31. package/src/format.ts +359 -0
  32. package/src/guidance.ts +163 -0
  33. package/src/index.ts +17 -0
  34. package/src/lsp-state.ts +82 -0
  35. package/src/lsp.ts +481 -0
  36. package/src/manager/manager-client-state.ts +34 -0
  37. package/src/manager/manager-diagnostics.ts +139 -0
  38. package/src/manager/manager-helpers.ts +39 -0
  39. package/src/manager/manager-project-info.ts +46 -0
  40. package/src/manager/manager-stale-resync.ts +47 -0
  41. package/src/manager/manager-types.ts +39 -0
  42. package/src/manager/manager-workspace-recovery.ts +83 -0
  43. package/src/manager/manager-workspace-symbol.ts +18 -0
  44. package/src/manager/manager.ts +550 -0
  45. package/src/overrides.ts +173 -0
  46. package/src/pattern-matcher.ts +197 -0
  47. package/src/renderer.ts +120 -0
  48. package/src/scanner.ts +153 -0
  49. package/src/search-fallback.ts +98 -0
  50. package/src/service-registry.ts +153 -0
  51. package/src/settings-registration.ts +292 -0
  52. package/{summary.ts → src/summary.ts} +44 -9
  53. package/src/tool-actions.ts +430 -0
  54. package/src/tree-persist.ts +48 -0
  55. package/src/tsconfig-scope.ts +156 -0
  56. package/{types.ts → src/types.ts} +123 -0
  57. package/src/ui.ts +358 -0
  58. package/{utils.ts → src/utils.ts} +8 -25
  59. package/src/workspace-sentinels.ts +114 -0
  60. package/bash-guard.ts +0 -58
  61. package/config.ts +0 -99
  62. package/defaults.json +0 -40
  63. package/format.ts +0 -190
  64. package/guidance.ts +0 -140
  65. package/lsp.ts +0 -375
  66. package/manager.ts +0 -396
  67. package/overrides.ts +0 -95
  68. package/recent-paths.ts +0 -126
  69. package/runtime-state.ts +0 -113
  70. package/tool-actions.ts +0 -211
  71. package/tsconfig.json +0 -5
  72. package/ui.ts +0 -303
package/bash-guard.ts DELETED
@@ -1,58 +0,0 @@
1
- const SEMANTIC_PROMPT_PATTERNS = [
2
- /\bdefinition\b/i,
3
- /\bfind(?: all)? references?\b/i,
4
- /\breferences?\b/i,
5
- /\busages?\b/i,
6
- /\bsymbols?\b/i,
7
- /\bhover\b/i,
8
- /\brename\b/i,
9
- /\bcode actions?\b/i,
10
- /\bdiagnostics?\b/i,
11
- /\btype errors?\b/i,
12
- /\bwarning\b/i,
13
- ];
14
-
15
- const TEXT_SEARCH_COMMAND_PATTERNS = [
16
- /\brg\b/,
17
- /\bripgrep\b/,
18
- /\bgrep\b/,
19
- /\bgit\s+grep\b/,
20
- /\back\b/,
21
- /\bag\b/,
22
- ];
23
-
24
- /**
25
- * Returns a redirect message when the agent is about to run a text-search
26
- * command (rg/grep/etc.) for a clearly semantic question (definitions,
27
- * references, symbols, diagnostics…) over files that already have active LSP
28
- * coverage. In that combination the lsp tool is strictly more accurate, so we
29
- * block the bash call and steer the agent to lsp. Returns null otherwise — we
30
- * don't interfere with plain-text searches or with searches that fall outside
31
- * the active LSP coverage.
32
- */
33
- export function shouldBlockSemanticBashSearch(
34
- command: string,
35
- prompt: string,
36
- relevantPaths: string[],
37
- hasRelevantCoverage: boolean,
38
- ): string | null {
39
- if (!hasRelevantCoverage) return null;
40
- if (!isSemanticPrompt(prompt) || !isTextSearchCommand(command)) return null;
41
-
42
- const visiblePaths = relevantPaths.slice(0, 2).join(", ");
43
- const targetText = visiblePaths ? ` in ${visiblePaths}` : " in files with active LSP coverage";
44
-
45
- return [
46
- "Use the lsp tool instead of bash text search for semantic queries.",
47
- `Active LSP coverage is available${targetText}.`,
48
- "Prefer lsp for definitions, references, symbols, hover, rename planning, code actions, and diagnostics.",
49
- ].join(" ");
50
- }
51
-
52
- export function isSemanticPrompt(prompt: string): boolean {
53
- return SEMANTIC_PROMPT_PATTERNS.some((pattern) => pattern.test(prompt));
54
- }
55
-
56
- export function isTextSearchCommand(command: string): boolean {
57
- return TEXT_SEARCH_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
58
- }
package/config.ts DELETED
@@ -1,99 +0,0 @@
1
- // LSP server configuration — load defaults, merge with project overrides.
2
-
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import type { LspConfig, ServerConfig } from "./types.ts";
6
-
7
- // Load defaults at module level — resolve relative to this file.
8
- // pi loads extensions via jiti, which always provides __dirname.
9
- const DEFAULTS: LspConfig = JSON.parse(
10
- fs.readFileSync(path.join(__dirname, "defaults.json"), "utf-8"),
11
- ) as LspConfig;
12
-
13
- // ── Public API ────────────────────────────────────────────────────────
14
-
15
- /**
16
- * Load LSP config: built-in defaults merged with optional `.pi-lsp.json`
17
- * from the project root. Project config takes precedence.
18
- */
19
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward merge logic
20
- export function loadConfig(cwd: string): LspConfig {
21
- const defaults = DEFAULTS;
22
- const projectOverrides = loadProjectConfig(cwd);
23
-
24
- // Start from defaults, merge project overrides if present
25
- const merged: Record<string, ServerConfig> = { ...defaults.servers };
26
-
27
- if (projectOverrides) {
28
- for (const [name, override] of Object.entries(projectOverrides.servers)) {
29
- if (override.enabled === false) {
30
- delete merged[name];
31
- continue;
32
- }
33
- if (merged[name]) {
34
- merged[name] = { ...merged[name], ...override };
35
- } else {
36
- // New server from project config — must have all required fields
37
- if (override.command && override.fileTypes && override.rootMarkers) {
38
- merged[name] = override as ServerConfig;
39
- }
40
- }
41
- }
42
- }
43
-
44
- // Apply PI_LSP_SERVERS filter (always, even without project config)
45
- const allowList = getServerAllowList();
46
- if (allowList) {
47
- for (const name of Object.keys(merged)) {
48
- if (!allowList.has(name)) {
49
- delete merged[name];
50
- }
51
- }
52
- }
53
-
54
- return { servers: merged };
55
- }
56
-
57
- /**
58
- * Find which server config handles a given file extension.
59
- * Returns [serverName, config] or null.
60
- */
61
- export function getServerForFile(
62
- config: LspConfig,
63
- filePath: string,
64
- ): [string, ServerConfig] | null {
65
- const ext = path.extname(filePath).slice(1).toLowerCase();
66
- if (!ext) return null;
67
-
68
- for (const [name, server] of Object.entries(config.servers)) {
69
- if (server.fileTypes.includes(ext)) {
70
- return [name, server];
71
- }
72
- }
73
- return null;
74
- }
75
-
76
- // ── Private ───────────────────────────────────────────────────────────
77
-
78
- function loadProjectConfig(cwd: string): LspConfig | null {
79
- const jsonPath = path.join(cwd, ".pi-lsp.json");
80
-
81
- if (fs.existsSync(jsonPath)) {
82
- try {
83
- const content = fs.readFileSync(jsonPath, "utf-8");
84
- return JSON.parse(content) as LspConfig;
85
- } catch {}
86
- }
87
-
88
- return null;
89
- }
90
-
91
- function getServerAllowList(): Set<string> | null {
92
- const env = process.env.PI_LSP_SERVERS;
93
- if (!env) return null;
94
- const names = env
95
- .split(",")
96
- .map((s) => s.trim())
97
- .filter(Boolean);
98
- return names.length > 0 ? new Set(names) : null;
99
- }
package/defaults.json DELETED
@@ -1,40 +0,0 @@
1
- {
2
- "servers": {
3
- "typescript-language-server": {
4
- "command": "typescript-language-server",
5
- "args": ["--stdio"],
6
- "fileTypes": ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs"],
7
- "rootMarkers": ["tsconfig.json", "jsconfig.json", "package.json"]
8
- },
9
- "pyright": {
10
- "command": "pyright-langserver",
11
- "args": ["--stdio"],
12
- "fileTypes": ["py", "pyi"],
13
- "rootMarkers": [
14
- "pyproject.toml",
15
- "setup.py",
16
- "setup.cfg",
17
- "requirements.txt",
18
- "pyrightconfig.json"
19
- ]
20
- },
21
- "rust-analyzer": {
22
- "command": "rust-analyzer",
23
- "args": [],
24
- "fileTypes": ["rs"],
25
- "rootMarkers": ["Cargo.toml"]
26
- },
27
- "gopls": {
28
- "command": "gopls",
29
- "args": ["serve"],
30
- "fileTypes": ["go", "mod"],
31
- "rootMarkers": ["go.mod", "go.sum"]
32
- },
33
- "clangd": {
34
- "command": "clangd",
35
- "args": ["--background-index"],
36
- "fileTypes": ["c", "h", "cpp", "hpp", "cc", "cxx", "hxx", "c++", "h++"],
37
- "rootMarkers": ["compile_commands.json", "CMakeLists.txt", ".clangd", "Makefile"]
38
- }
39
- }
40
- }
package/format.ts DELETED
@@ -1,190 +0,0 @@
1
- // LSP result formatting — converts LSP response types into readable text.
2
-
3
- import * as path from "node:path";
4
- import type {
5
- CodeAction,
6
- DocumentSymbol,
7
- Hover,
8
- Location,
9
- LocationLink,
10
- MarkedString,
11
- MarkupContent,
12
- SymbolInformation,
13
- WorkspaceEdit,
14
- } from "./types.ts";
15
- import { uriToFile } from "./utils.ts";
16
-
17
- // ── Hover ─────────────────────────────────────────────────────────────
18
-
19
- export function formatHover(hover: Hover): string {
20
- const contents = hover.contents;
21
-
22
- if (typeof contents === "string") return contents;
23
- if ("value" in contents) {
24
- const mc = contents as MarkupContent | { language: string; value: string };
25
- if ("kind" in mc) return mc.value;
26
- return `\`\`\`${mc.language}\n${mc.value}\n\`\`\``;
27
- }
28
- if (Array.isArray(contents)) {
29
- return (contents as MarkedString[])
30
- .map((c) => (typeof c === "string" ? c : `\`\`\`${c.language}\n${c.value}\n\`\`\``))
31
- .join("\n\n");
32
- }
33
-
34
- return String(contents);
35
- }
36
-
37
- // ── Locations ─────────────────────────────────────────────────────────
38
-
39
- export function formatLocations(label: string, locations: Location[]): string {
40
- if (locations.length === 1) {
41
- const loc = locations[0];
42
- const file = relPath(uriToFile(loc.uri));
43
- const line = loc.range.start.line + 1;
44
- const col = loc.range.start.character + 1;
45
- return `${label}: ${file}:${line}:${col}`;
46
- }
47
-
48
- const lines = [`${label} (${locations.length} locations):\n`];
49
- for (const loc of locations) {
50
- const file = relPath(uriToFile(loc.uri));
51
- const line = loc.range.start.line + 1;
52
- const col = loc.range.start.character + 1;
53
- lines.push(`- ${file}:${line}:${col}`);
54
- }
55
- return lines.join("\n");
56
- }
57
-
58
- export function normalizeLocations(result: Location | Location[] | LocationLink[]): Location[] {
59
- if (Array.isArray(result)) {
60
- return result.map((item) => {
61
- if ("targetUri" in item) {
62
- const link = item as LocationLink;
63
- return { uri: link.targetUri, range: link.targetSelectionRange };
64
- }
65
- return item as Location;
66
- });
67
- }
68
- return [result as Location];
69
- }
70
-
71
- // ── Symbols ───────────────────────────────────────────────────────────
72
-
73
- export function formatDocumentSymbols(symbols: DocumentSymbol[], indent: number): string {
74
- const lines: string[] = [];
75
- const prefix = " ".repeat(indent);
76
-
77
- for (const sym of symbols) {
78
- const kind = symbolKindName(sym.kind);
79
- const line = sym.selectionRange.start.line + 1;
80
- const detail = sym.detail ? ` — ${sym.detail}` : "";
81
- lines.push(`${prefix}- ${kind} **${sym.name}**${detail} (line ${line})`);
82
-
83
- if (sym.children && sym.children.length > 0) {
84
- lines.push(formatDocumentSymbols(sym.children, indent + 1));
85
- }
86
- }
87
-
88
- return lines.join("\n");
89
- }
90
-
91
- export function formatSymbolInformation(symbols: SymbolInformation[]): string {
92
- const lines: string[] = [];
93
- for (const sym of symbols) {
94
- const kind = symbolKindName(sym.kind);
95
- const file = relPath(uriToFile(sym.location.uri));
96
- const line = sym.location.range.start.line + 1;
97
- const container = sym.containerName ? ` (in ${sym.containerName})` : "";
98
- lines.push(`- ${kind} **${sym.name}**${container} — ${file}:${line}`);
99
- }
100
- return lines.join("\n");
101
- }
102
-
103
- // ── Workspace Edits ───────────────────────────────────────────────────
104
-
105
- export function formatWorkspaceEdit(edit: WorkspaceEdit): string {
106
- const lines: string[] = ["Rename workspace edit:\n"];
107
-
108
- if (edit.changes) {
109
- for (const [uri, edits] of Object.entries(edit.changes)) {
110
- const file = relPath(uriToFile(uri));
111
- lines.push(`**${file}** (${edits.length} change(s))`);
112
- for (const e of edits) {
113
- const line = e.range.start.line + 1;
114
- lines.push(` Line ${line}: → "${e.newText}"`);
115
- }
116
- }
117
- }
118
-
119
- if (edit.documentChanges) {
120
- for (const dc of edit.documentChanges) {
121
- const file = relPath(uriToFile(dc.textDocument.uri));
122
- lines.push(`**${file}** (${dc.edits.length} change(s))`);
123
- for (const e of dc.edits) {
124
- const line = e.range.start.line + 1;
125
- lines.push(` Line ${line}: → "${e.newText}"`);
126
- }
127
- }
128
- }
129
-
130
- return lines.join("\n");
131
- }
132
-
133
- // ── Code Actions ──────────────────────────────────────────────────────
134
-
135
- export function formatCodeActions(actions: CodeAction[]): string {
136
- const lines = [`Available code actions (${actions.length}):\n`];
137
- for (const action of actions) {
138
- const kind = action.kind ? ` [${action.kind}]` : "";
139
- const preferred = action.isPreferred ? " ⭐" : "";
140
- lines.push(`- **${action.title}**${kind}${preferred}`);
141
- if (action.edit) {
142
- const fileCount = action.edit.changes
143
- ? Object.keys(action.edit.changes).length
144
- : (action.edit.documentChanges?.length ?? 0);
145
- lines.push(` Edits ${fileCount} file(s)`);
146
- }
147
- }
148
- return lines.join("\n");
149
- }
150
-
151
- // ── Symbol Kind Names ─────────────────────────────────────────────────
152
-
153
- const SYMBOL_KIND_NAMES: Record<number, string> = {
154
- 1: "File",
155
- 2: "Module",
156
- 3: "Namespace",
157
- 4: "Package",
158
- 5: "Class",
159
- 6: "Method",
160
- 7: "Property",
161
- 8: "Field",
162
- 9: "Constructor",
163
- 10: "Enum",
164
- 11: "Interface",
165
- 12: "Function",
166
- 13: "Variable",
167
- 14: "Constant",
168
- 15: "String",
169
- 16: "Number",
170
- 17: "Boolean",
171
- 18: "Array",
172
- 19: "Object",
173
- 20: "Key",
174
- 21: "Null",
175
- 22: "EnumMember",
176
- 23: "Struct",
177
- 24: "Event",
178
- 25: "Operator",
179
- 26: "TypeParameter",
180
- };
181
-
182
- function symbolKindName(kind: number): string {
183
- return SYMBOL_KIND_NAMES[kind] ?? `Kind(${kind})`;
184
- }
185
-
186
- // ── Helpers ───────────────────────────────────────────────────────────
187
-
188
- function relPath(filePath: string): string {
189
- return path.relative(process.cwd(), filePath);
190
- }
package/guidance.ts DELETED
@@ -1,140 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { LspManager } from "./manager.ts";
4
-
5
- export const lspPromptSnippet =
6
- "Use semantic code intelligence for hover, definitions, references, symbols, rename planning, code actions, and diagnostics in supported languages.";
7
-
8
- export const lspPromptGuidelines = [
9
- "Prefer the lsp tool over bash text search for supported source files when the task is semantic code navigation or diagnostics.",
10
- "Use lsp for hover, definitions, references, document symbols, rename planning, code actions, and diagnostics before falling back to grep-style shell search.",
11
- "Fall back to bash/read when LSP is unavailable, the file type is unsupported, or the task is plain-text search across docs, config files, or string literals.",
12
- ];
13
-
14
- type DiagnosticsManager = Pick<LspManager, "getRelevantOutstandingDiagnosticsSummaryText">;
15
-
16
- type GuidanceMessageLike = {
17
- customType?: string;
18
- details?: unknown;
19
- };
20
-
21
- export interface RuntimeGuidanceInput {
22
- pendingActivation: boolean;
23
- diagnosticsSummary: string | null;
24
- trackedFiles: string[];
25
- }
26
-
27
- export function computeTrackedDiagnosticsSummary(
28
- manager: DiagnosticsManager,
29
- inlineSeverity: number,
30
- trackedPaths: string[],
31
- ): string | null {
32
- if (trackedPaths.length === 0) return null;
33
- return manager.getRelevantOutstandingDiagnosticsSummaryText(trackedPaths, inlineSeverity);
34
- }
35
-
36
- export function buildRuntimeLspGuidance(input: RuntimeGuidanceInput): string | null {
37
- const lines: string[] = [];
38
- const fileHint = summarizeTrackedFiles(input.trackedFiles);
39
-
40
- if (input.pendingActivation) {
41
- lines.push(
42
- fileHint
43
- ? `LSP ready for semantic navigation on tracked source files (${fileHint}).`
44
- : "LSP ready for semantic navigation on the tracked source files.",
45
- );
46
- } else if (fileHint) {
47
- // After activation, surface the current tracked-file context so the agent
48
- // sees newly touched supported source files reflected in runtime guidance.
49
- // Dedup is the caller's job (fingerprint match → skip).
50
- lines.push(`LSP tracking source files: ${fileHint}.`);
51
- }
52
-
53
- if (input.diagnosticsSummary) {
54
- lines.push(input.diagnosticsSummary);
55
- }
56
-
57
- if (lines.length === 0) return null;
58
-
59
- return ["LSP guidance:", ...lines.map((line) => `- ${line}`)].join("\n");
60
- }
61
-
62
- /**
63
- * Fingerprint captures the parts of runtime guidance that persist across turns
64
- * — tracked source-file set and tracked diagnostics summary. `pendingActivation`
65
- * is a one-shot signal cleared after injection, so it's excluded; otherwise an
66
- * unchanged-state turn would never match the previously stored fingerprint.
67
- * Tracked files are included so newly opened supported files re-trigger
68
- * guidance even when diagnostics are unchanged.
69
- */
70
- export function runtimeGuidanceFingerprint(input: RuntimeGuidanceInput): string {
71
- // Canonicalize by sorting: registerQualifyingSourceInteraction moves the most
72
- // recent file to the front of trackedSourcePaths, so an order-sensitive join
73
- // would treat re-touching an already-tracked file as a state change and
74
- // re-inject guidance during ordinary back-and-forth edits.
75
- const canonical = [...input.trackedFiles].sort().join("|");
76
- return `${canonical}\u0000${input.diagnosticsSummary ?? ""}`;
77
- }
78
-
79
- export function extractPromptPathHints(prompt: string, cwd: string = process.cwd()): string[] {
80
- const tokens = prompt.match(/[A-Za-z0-9_./-]+/g) ?? [];
81
- const matches = new Set<string>();
82
-
83
- for (const token of tokens) {
84
- const candidate = normalizePromptPathHint(token);
85
- if (!candidate) continue;
86
-
87
- const resolved = path.resolve(cwd, candidate);
88
- if (!fs.existsSync(resolved)) continue;
89
-
90
- const relative = path.relative(cwd, resolved);
91
- if (relative === "") {
92
- matches.add(path.basename(resolved));
93
- continue;
94
- }
95
-
96
- if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
97
- matches.add(relative.replaceAll(path.sep, "/"));
98
- }
99
- }
100
-
101
- return Array.from(matches);
102
- }
103
-
104
- export function mergeRelevantPaths(
105
- promptPaths: string[],
106
- recentPaths: string[],
107
- maxEntries: number = 8,
108
- ): string[] {
109
- return Array.from(new Set([...promptPaths, ...recentPaths])).slice(0, maxEntries);
110
- }
111
-
112
- export function filterLspGuidanceMessages<T extends GuidanceMessageLike>(
113
- messages: T[],
114
- activeGuidanceToken: string | null,
115
- ): T[] {
116
- return messages.filter((message) => {
117
- if (message.customType !== "lsp-guidance") return true;
118
- if (!activeGuidanceToken) return false;
119
- return getGuidanceToken(message.details) === activeGuidanceToken;
120
- });
121
- }
122
-
123
- function summarizeTrackedFiles(files: string[], maxFiles: number = 2): string {
124
- if (files.length === 0) return "";
125
- const shown = files.slice(0, maxFiles).join(", ");
126
- const remaining = files.length - maxFiles;
127
- return remaining > 0 ? `${shown}, +${remaining} more` : shown;
128
- }
129
-
130
- function getGuidanceToken(details: unknown): string | null {
131
- if (!details || typeof details !== "object") return null;
132
- const token = (details as { guidanceToken?: unknown }).guidanceToken;
133
- return typeof token === "string" ? token : null;
134
- }
135
-
136
- function normalizePromptPathHint(token: string): string | null {
137
- const cleaned = token.replace(/^[`'"([]+|[`'"),.:;\]]+$/g, "");
138
- if (cleaned.length < 2) return null;
139
- return cleaned;
140
- }