sift-light 1.0.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/CONTRIBUTING.md +19 -0
  3. package/LICENSE +661 -0
  4. package/README.md +177 -0
  5. package/README.zh-CN.md +177 -0
  6. package/SECURITY.md +25 -0
  7. package/package.json +142 -0
  8. package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
  9. package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
  10. package/plugins/sift-light/.mcp.json +24 -0
  11. package/plugins/sift-light/LICENSE +661 -0
  12. package/plugins/sift-light/concept-worker.mjs +567 -0
  13. package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
  14. package/plugins/sift-light/hooks/hooks.json +16 -0
  15. package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
  16. package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
  17. package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
  18. package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
  19. package/plugins/sift-light/kimi.plugin.json +36 -0
  20. package/plugins/sift-light/mcp-server.mjs +1 -0
  21. package/plugins/sift-light/omp-extension.mjs +20914 -0
  22. package/plugins/sift-light/package.json +11 -0
  23. package/plugins/sift-light/syntax-worker.mjs +280 -0
  24. package/plugins/sift-light/syntax-worker.toml +2 -0
  25. package/src/analysis-evidence.ts +43 -0
  26. package/src/analysis-limits.ts +27 -0
  27. package/src/analysis-store.ts +559 -0
  28. package/src/analysis-term-pages.ts +73 -0
  29. package/src/analysis-types.ts +162 -0
  30. package/src/audit-search.ts +391 -0
  31. package/src/capped-lines.ts +112 -0
  32. package/src/concept-embedding-cache.ts +186 -0
  33. package/src/concept-inference.ts +218 -0
  34. package/src/concept-model.ts +96 -0
  35. package/src/concept-search.ts +577 -0
  36. package/src/concept-setup.ts +75 -0
  37. package/src/concept-source-generation.ts +268 -0
  38. package/src/concept-worker.mjs +567 -0
  39. package/src/concept-worker.ts +210 -0
  40. package/src/config-reader.ts +283 -0
  41. package/src/config.ts +37 -0
  42. package/src/context-budget.ts +26 -0
  43. package/src/discovery-errors.ts +2 -0
  44. package/src/errors.ts +95 -0
  45. package/src/evidence-candidates.ts +344 -0
  46. package/src/evidence-ranking.ts +48 -0
  47. package/src/evidence-service.ts +1047 -0
  48. package/src/evidence-validation.ts +466 -0
  49. package/src/evidence-validity.ts +33 -0
  50. package/src/excerpt.ts +53 -0
  51. package/src/file-discovery.ts +150 -0
  52. package/src/file-metadata-filter.ts +46 -0
  53. package/src/format.ts +407 -0
  54. package/src/git-diff.ts +195 -0
  55. package/src/git-process.ts +189 -0
  56. package/src/git-repository.ts +367 -0
  57. package/src/git-source.ts +385 -0
  58. package/src/historical-paths.ts +205 -0
  59. package/src/hybrid-search.ts +340 -0
  60. package/src/import-model.ts +611 -0
  61. package/src/import-navigation.ts +100 -0
  62. package/src/import-resolution.ts +240 -0
  63. package/src/index.ts +97 -0
  64. package/src/inspect-batch.ts +265 -0
  65. package/src/inspect.ts +271 -0
  66. package/src/language-capabilities.ts +157 -0
  67. package/src/language-capability-definitions.ts +204 -0
  68. package/src/literal-search.ts +25 -0
  69. package/src/mcp-cli.ts +21 -0
  70. package/src/mcp-model-output.ts +120 -0
  71. package/src/mcp-output.ts +9 -0
  72. package/src/mcp-semantic-judge.ts +39 -0
  73. package/src/mcp-server.mjs +14064 -0
  74. package/src/mcp-server.ts +157 -0
  75. package/src/mcp-stdio.ts +114 -0
  76. package/src/mcp.ts +631 -0
  77. package/src/model-error.ts +120 -0
  78. package/src/multi-term-search.ts +122 -0
  79. package/src/omp-index.ts +277 -0
  80. package/src/omp-marketplace-entry.ts +7 -0
  81. package/src/operation-lifecycle.ts +413 -0
  82. package/src/operation-output.ts +100 -0
  83. package/src/owned-parallel.ts +22 -0
  84. package/src/owned-process.ts +102 -0
  85. package/src/owned-task-queue.ts +38 -0
  86. package/src/package-version.ts +3 -0
  87. package/src/path-policy.ts +203 -0
  88. package/src/pi-search-policy.ts +33 -0
  89. package/src/prompt-guidelines.ts +54 -0
  90. package/src/python-outline.ts +231 -0
  91. package/src/record-value.ts +3 -0
  92. package/src/redaction.ts +95 -0
  93. package/src/request-contract-catalog.ts +215 -0
  94. package/src/request-contract-error.ts +96 -0
  95. package/src/request-contract.ts +481 -0
  96. package/src/request.ts +132 -0
  97. package/src/result-statistics.ts +178 -0
  98. package/src/rg.ts +574 -0
  99. package/src/ripgrep-diagnostics.ts +122 -0
  100. package/src/ripgrep-executable.ts +43 -0
  101. package/src/runtime.ts +53 -0
  102. package/src/scan-revisions.ts +148 -0
  103. package/src/script-runtime.ts +11 -0
  104. package/src/search-policy-commands.ts +170 -0
  105. package/src/search-policy-hook.ts +61 -0
  106. package/src/search-policy-recovery.ts +388 -0
  107. package/src/search-policy-shell.ts +280 -0
  108. package/src/search-policy.ts +102 -0
  109. package/src/search-retention.ts +77 -0
  110. package/src/semantic-judge-batches.ts +135 -0
  111. package/src/semantic-judge.ts +519 -0
  112. package/src/service.ts +945 -0
  113. package/src/session-summary.ts +100 -0
  114. package/src/snapshot-store.ts +203 -0
  115. package/src/source-access.ts +288 -0
  116. package/src/source-continuations.ts +168 -0
  117. package/src/source-document.ts +277 -0
  118. package/src/source-inspection.ts +731 -0
  119. package/src/source-pages.ts +112 -0
  120. package/src/source.ts +286 -0
  121. package/src/structural-search.ts +125 -0
  122. package/src/structure.ts +259 -0
  123. package/src/summary-previews.ts +96 -0
  124. package/src/summary.ts +79 -0
  125. package/src/syntax-facts.ts +464 -0
  126. package/src/syntax-fields.ts +161 -0
  127. package/src/syntax-search.ts +361 -0
  128. package/src/syntax-tree.ts +30 -0
  129. package/src/syntax-types.ts +72 -0
  130. package/src/syntax-worker.mjs +280 -0
  131. package/src/syntax-worker.toml +2 -0
  132. package/src/syntax-worker.ts +143 -0
  133. package/src/syntax.ts +259 -0
  134. package/src/test-navigation-facts.ts +261 -0
  135. package/src/test-navigation.ts +543 -0
  136. package/src/tool-schema.ts +321 -0
  137. package/src/tui/dashboard.ts +107 -0
  138. package/src/tui/layout.ts +291 -0
  139. package/src/tui/palette.ts +73 -0
  140. package/src/tui/presentation.ts +240 -0
  141. package/src/tui/renderers.ts +106 -0
  142. package/src/types.ts +368 -0
  143. package/src/validation-output.ts +148 -0
  144. package/src/validation-types.ts +48 -0
  145. package/src/workspace-files.ts +142 -0
@@ -0,0 +1,120 @@
1
+ import { types } from "node:util";
2
+ import {
3
+ boundedRequestContractDetails,
4
+ isSiftLightDiagnosticError,
5
+ MAX_REQUEST_RECOVERY_BYTES,
6
+ } from "./request-contract.js";
7
+ import type { SiftLightDiagnosticError } from "./errors.js";
8
+ import type { RequestContractDetails } from "./request-contract.js";
9
+
10
+ const MAX_RAW_ERROR_SCAN_CHARACTERS = 4_096;
11
+ const MAX_MODEL_ERROR_CHARACTERS = 1_024;
12
+ const MODEL_ERROR_PREFIX = "sift-light failed:";
13
+
14
+ function errorMessage(error: unknown): string {
15
+ try {
16
+ if (types.isNativeError(error)) {
17
+ const message = Object.getOwnPropertyDescriptor(error, "message");
18
+ if (!message) return "unknown failure";
19
+ return typeof message.value === "string" ? message.value : "unreadable failure";
20
+ }
21
+ if (error === null) return "null";
22
+ switch (typeof error) {
23
+ case "string":
24
+ return error;
25
+ case "number":
26
+ return String(error);
27
+ case "boolean":
28
+ return error ? "true" : "false";
29
+ case "undefined":
30
+ return "undefined";
31
+ case "bigint":
32
+ return "bigint failure";
33
+ case "symbol":
34
+ return "symbol failure";
35
+ case "function":
36
+ case "object":
37
+ return "non-error failure";
38
+ default:
39
+ return "unreadable failure";
40
+ }
41
+ } catch {
42
+ return "unreadable failure";
43
+ }
44
+ }
45
+
46
+ /** One bounded model-facing diagnostic; never serialize causes, stacks, or repeated request text. */
47
+ export function modelErrorText(error: unknown): string {
48
+ if (isSiftLightDiagnosticError(error)) return requestContractErrorText(error);
49
+ const raw = errorMessage(error);
50
+ const normalized = raw
51
+ .slice(0, MAX_RAW_ERROR_SCAN_CHARACTERS)
52
+ .toWellFormed()
53
+ .replace(/\s+/gu, " ")
54
+ .trim();
55
+ const message = normalized.replace(/^(?:sift-light failed:\s*)+/u, "") || "unknown failure";
56
+ const text = `${MODEL_ERROR_PREFIX} ${message}`;
57
+ if (text.length <= MAX_MODEL_ERROR_CHARACTERS) return text;
58
+ return `${text.slice(0, MAX_MODEL_ERROR_CHARACTERS - 1).toWellFormed()}…`;
59
+ }
60
+
61
+ /**
62
+ * Contract recovery is a copy protocol, so it must bypass the generic model
63
+ * error whitespace normalization and character truncation. The contract
64
+ * builder omits oversized next requests instead of allowing a false exact
65
+ * retry to be emitted.
66
+ */
67
+ export function requestContractErrorText(error: SiftLightDiagnosticError): string {
68
+ return requestContractProjection(error).text;
69
+ }
70
+
71
+ export interface RequestContractProjection {
72
+ details: RequestContractDetails;
73
+ text: string;
74
+ }
75
+
76
+ function projectRequestContract(
77
+ projected: RequestContractDetails,
78
+ serialized: string,
79
+ message: string,
80
+ ): string {
81
+ const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
82
+ const recovery = projected.recovery;
83
+ const next = recovery.nextRequest
84
+ ? "\nCopy the nested recovery.nextRequest object unchanged; do not repeat the original query."
85
+ : `\nRecovery action: ${recovery.action}. ${recovery.reason}`;
86
+ // Put the machine payload first so simple host adapters that locate the
87
+ // first `nextRequest` marker see the exact nested request object.
88
+ return `\nError details: ${serialized}\n${prefix}${next}`;
89
+ }
90
+
91
+ /** Build the one bounded contract projection consumed by structured and text hosts. */
92
+ export function requestContractProjection(
93
+ error: SiftLightDiagnosticError,
94
+ ): RequestContractProjection {
95
+ const details = boundedRequestContractDetails(error.details);
96
+ const serializedDetails = JSON.stringify(details);
97
+ const boundedMessage = error.message.toWellFormed().slice(0, 1_024);
98
+ const result = projectRequestContract(details, serializedDetails, boundedMessage);
99
+ if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES) return { details, text: result };
100
+ const compact: RequestContractDetails = {
101
+ code: boundedMessage.length > 0 ? details.code : "E_REQUEST_CONTRACT_PAYLOAD",
102
+ ...(details.mode ? { mode: details.mode } : {}),
103
+ issues: [
104
+ {
105
+ field: "<payload>",
106
+ reason: "The visible request-contract error exceeded its bounded payload budget.",
107
+ },
108
+ ],
109
+ recovery: {
110
+ action: "manual" as const,
111
+ reason: "Exact recovery was omitted; correct the request explicitly.",
112
+ },
113
+ };
114
+ const compactText = projectRequestContract(
115
+ compact,
116
+ JSON.stringify(compact),
117
+ "The request-contract error exceeded the bounded payload budget; correct the request explicitly.",
118
+ );
119
+ return { details: compact, text: compactText };
120
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ ANALYSIS_METADATA_RESERVE_BYTES,
3
+ MAX_ANY_OF_TERMS,
4
+ MAX_ANY_OF_TOTAL_TERMS,
5
+ MAX_ANALYSIS_RESULTS,
6
+ MAX_ANALYSIS_STORAGE_BYTES,
7
+ MAX_LITERAL_TERM_BYTES,
8
+ MIN_ANY_OF_TERMS,
9
+ } from "./analysis-limits.js";
10
+ import { sourceEvidence } from "./analysis-evidence.js";
11
+ import type { AnalysisItem } from "./analysis-types.js";
12
+ import { SiftLightError } from "./errors.js";
13
+ import type { EvidenceCandidateFile } from "./evidence-candidates.js";
14
+ import { literalOccurrences } from "./literal-search.js";
15
+
16
+ export function validateAnyOf(value: string[] | undefined): string[] | undefined {
17
+ if (value === undefined) return undefined;
18
+ if (
19
+ !Array.isArray(value) ||
20
+ value.length < MIN_ANY_OF_TERMS ||
21
+ value.length > MAX_ANY_OF_TOTAL_TERMS ||
22
+ value.some(
23
+ (term) =>
24
+ typeof term !== "string" ||
25
+ term.length === 0 ||
26
+ !term.isWellFormed() ||
27
+ /[\r\n\0]/.test(term) ||
28
+ Buffer.byteLength(term) > MAX_LITERAL_TERM_BYTES,
29
+ ) ||
30
+ new Set(value).size !== value.length
31
+ ) {
32
+ throw new SiftLightError(
33
+ `anyOf requires ${String(MIN_ANY_OF_TERMS)}–${String(MAX_ANY_OF_TOTAL_TERMS)} distinct, nonempty, well-formed, single-line literal terms of at most ${String(MAX_LITERAL_TERM_BYTES)} UTF-8 bytes; requests above ${String(MAX_ANY_OF_TERMS)} terms are safely chunked`,
34
+ );
35
+ }
36
+ return value;
37
+ }
38
+
39
+ export interface MultiTermExpansion {
40
+ items: AnalysisItem[];
41
+ partial: boolean;
42
+ reasons: string[];
43
+ }
44
+
45
+ /** Expands candidate files locally so alternation choice never hides an overlapping input term. */
46
+ export function expandMultiTermCandidates(
47
+ files: readonly EvidenceCandidateFile[],
48
+ terms: readonly string[],
49
+ changedLinesOnly: boolean,
50
+ ): MultiTermExpansion {
51
+ const items: (AnalysisItem & { termIndex: number })[] = [];
52
+ const reasons = new Set<string>();
53
+ const orderedFiles = files.toSorted((left, right) =>
54
+ left.document.path.localeCompare(right.document.path),
55
+ );
56
+ for (const file of orderedFiles) {
57
+ if (!file.document.utf8) {
58
+ reasons.add(
59
+ `${file.document.path}: exact multi-term evidence requires lossless UTF-8 source`,
60
+ );
61
+ }
62
+ }
63
+ let serializedBytes = 0;
64
+ let exhausted = false;
65
+ for (let termIndex = 0; termIndex < terms.length && !exhausted; termIndex++) {
66
+ const term = terms[termIndex];
67
+ if (term === undefined) throw new Error("Missing validated anyOf term");
68
+ for (const file of orderedFiles) {
69
+ if (!file.document.utf8) continue;
70
+ const allowed = changedLinesOnly ? file.changedRanges : undefined;
71
+ for (const range of literalOccurrences(file.document, term, allowed)) {
72
+ const match = sourceEvidence(file.document, range);
73
+ const item: AnalysisItem & { termIndex: number } = {
74
+ path: file.document.path,
75
+ line: match.line,
76
+ label: `Exact literal occurrence for ${JSON.stringify(term)}`,
77
+ excerpt: match.excerpt,
78
+ source: file.document.reference,
79
+ range,
80
+ details: {
81
+ kind: "literal-term",
82
+ term,
83
+ termIndex,
84
+ excerptRange: match.excerptRange,
85
+ excerptTruncated: match.excerptTruncated,
86
+ },
87
+ termIndex,
88
+ };
89
+ const itemBytes = Buffer.byteLength(JSON.stringify(item)) + 1;
90
+ if (
91
+ items.length >= MAX_ANALYSIS_RESULTS ||
92
+ serializedBytes + itemBytes >=
93
+ MAX_ANALYSIS_STORAGE_BYTES - ANALYSIS_METADATA_RESERVE_BYTES
94
+ ) {
95
+ reasons.add("Exact multi-term retention reached the 50,000-item / 32 MiB analysis limit");
96
+ exhausted = true;
97
+ break;
98
+ }
99
+ items.push(item);
100
+ serializedBytes += itemBytes;
101
+ }
102
+ if (exhausted) break;
103
+ }
104
+ }
105
+ return {
106
+ items: items.map(({ termIndex: _termIndex, ...item }) => item),
107
+ partial: reasons.size > 0,
108
+ reasons: [...reasons],
109
+ };
110
+ }
111
+
112
+ export function retainedTermCounts(
113
+ terms: readonly string[],
114
+ items: readonly AnalysisItem[],
115
+ ): { term: string; retainedOccurrences: number }[] {
116
+ const counts = new Map(terms.map((term) => [term, 0]));
117
+ for (const item of items) {
118
+ const term = item.details?.term;
119
+ if (typeof term === "string" && counts.has(term)) counts.set(term, (counts.get(term) ?? 0) + 1);
120
+ }
121
+ return terms.map((term) => ({ term, retainedOccurrences: counts.get(term) ?? 0 }));
122
+ }
@@ -0,0 +1,277 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import {
4
+ DEFAULT_SEMANTIC_JUDGE_CONFIG,
5
+ normalizeSearchEnforcement,
6
+ readSiftLightConfigFile,
7
+ resolveSiftLightConfigPath,
8
+ SIFT_LIGHT_CONFIG_ENV,
9
+ type SiftLightConfig,
10
+ } from "./config-reader.js";
11
+ import { resolveContextBudget } from "./context-budget.js";
12
+ import { createRipgrepRunner } from "./rg.js";
13
+ import { createCtagsStructureProvider } from "./structure.js";
14
+ import { SiftLightRuntime } from "./runtime.js";
15
+ import { SESSION_STATUS_KEY } from "./session-summary.js";
16
+ import { SiftLightService, type SiftLightInput } from "./service.js";
17
+ import { siftLightPromptGuidelines } from "./prompt-guidelines.js";
18
+ import {
19
+ renderSiftLightCall,
20
+ renderSiftLightResult,
21
+ type SiftLightToolResult,
22
+ } from "./tui/renderers.js";
23
+ import {
24
+ PREFERRED_SEARCH_GUIDANCE,
25
+ SEARCH_POLICY_GUIDANCE,
26
+ SearchPolicy,
27
+ } from "./search-policy.js";
28
+ import { siftLightSchema } from "./tool-schema.js";
29
+ import { modelErrorText } from "./model-error.js";
30
+ import { createSemanticJudgeIntegration } from "./semantic-judge.js";
31
+
32
+ const SIFT_LIGHT_LABEL = "sift-light";
33
+ const OMP_REPLACED_SEARCH_TOOLS = new Set(["grep", "glob"]);
34
+ type OmpToolInput = SiftLightInput & { i?: unknown };
35
+
36
+ function normalizeOmpToolInput(params: OmpToolInput): SiftLightInput {
37
+ if (!Object.hasOwn(params, "i")) return params;
38
+ const input = { ...params };
39
+ delete input.i;
40
+ return input;
41
+ }
42
+
43
+ interface OmpTheme {
44
+ bold(value: string): string;
45
+ fg(color: string, value: string): string;
46
+ }
47
+
48
+ interface OmpContextUsage {
49
+ tokens: number | null;
50
+ contextWindow: number;
51
+ percent: number | null;
52
+ }
53
+
54
+ interface OmpExtensionContext {
55
+ cwd: string;
56
+ getContextUsage(): OmpContextUsage | undefined;
57
+ ui: {
58
+ setStatus(key: string, text: string | undefined): void;
59
+ };
60
+ }
61
+
62
+ interface OmpToolCallEvent {
63
+ toolName: string;
64
+ input: unknown;
65
+ }
66
+
67
+ interface OmpBeforeAgentStartEvent {
68
+ systemPrompt: string[];
69
+ }
70
+
71
+ interface OmpRenderOptions {
72
+ expanded: boolean;
73
+ isPartial: boolean;
74
+ }
75
+
76
+ interface OmpToolDefinition {
77
+ name: string;
78
+ label: string;
79
+ description: string;
80
+ approval?: "read" | "write" | "exec";
81
+ promptSnippet: string;
82
+ promptGuidelines: string[];
83
+ parameters: unknown;
84
+ execute(
85
+ toolCallId: string,
86
+ params: OmpToolInput,
87
+ signal: AbortSignal | undefined,
88
+ onUpdate: unknown,
89
+ ctx: OmpExtensionContext,
90
+ ): Promise<SiftLightToolResult>;
91
+ renderCall?(
92
+ params: SiftLightInput,
93
+ options: unknown,
94
+ theme: OmpTheme,
95
+ ): ReturnType<typeof renderSiftLightCall>;
96
+ renderResult?(
97
+ result: SiftLightToolResult,
98
+ options: OmpRenderOptions,
99
+ theme: OmpTheme,
100
+ args?: SiftLightInput,
101
+ ): ReturnType<typeof renderSiftLightResult>;
102
+ }
103
+
104
+ interface OmpExtensionAPI {
105
+ registerTool(tool: OmpToolDefinition): void;
106
+ on(
107
+ event: "session_start",
108
+ handler: (event: unknown, ctx: OmpExtensionContext) => void | Promise<void>,
109
+ ): void;
110
+ on(
111
+ event: "before_agent_start",
112
+ handler: (
113
+ event: OmpBeforeAgentStartEvent,
114
+ ctx: OmpExtensionContext,
115
+ ) => void | { systemPrompt?: string[] } | Promise<void | { systemPrompt?: string[] }>,
116
+ ): void;
117
+ on(
118
+ event: "tool_call",
119
+ handler: (
120
+ event: OmpToolCallEvent,
121
+ ctx: OmpExtensionContext,
122
+ ) => void | { block: true; reason: string } | Promise<void | { block: true; reason: string }>,
123
+ ): void;
124
+ on(
125
+ event: "session_shutdown",
126
+ handler: (event: unknown, ctx: OmpExtensionContext) => void | Promise<void>,
127
+ ): void;
128
+ getActiveTools(): string[];
129
+ setActiveTools(toolNames: string[]): Promise<void>;
130
+ }
131
+
132
+ function expandTilde(path: string): string {
133
+ if (path === "~") return homedir();
134
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
135
+ return path;
136
+ }
137
+
138
+ function ompProfile(): string | undefined {
139
+ const value = process.env.OMP_PROFILE ?? process.env.PI_PROFILE;
140
+ if (value === undefined) return undefined;
141
+ const profile = value.trim();
142
+ if (!profile || profile === "default") return undefined;
143
+ // OMP validates this before loading extensions. Keep direct SDK imports safe
144
+ // as well, without allowing an environment value to escape the profile root.
145
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(profile) || profile.endsWith(".")) return undefined;
146
+ return profile;
147
+ }
148
+
149
+ /** Resolve the same profile-scoped config directory that OMP exposes to extensions. */
150
+ function ompAgentDir(): string {
151
+ const configured = process.env.PI_CODING_AGENT_DIR;
152
+ if (configured) return expandTilde(configured);
153
+ const configDir = process.env.PI_CONFIG_DIR || ".omp";
154
+ const profile = ompProfile();
155
+ return profile
156
+ ? join(homedir(), configDir, "profiles", profile, "agent")
157
+ : join(homedir(), configDir, "agent");
158
+ }
159
+
160
+ function selectSearchTools(pi: OmpExtensionAPI, replaceAlternatives: boolean): string[] {
161
+ const current = pi.getActiveTools();
162
+ const next = replaceAlternatives
163
+ ? current.filter((tool) => !OMP_REPLACED_SEARCH_TOOLS.has(tool))
164
+ : [...current];
165
+ if (!next.includes(SIFT_LIGHT_LABEL)) next.push(SIFT_LIGHT_LABEL);
166
+ return next;
167
+ }
168
+
169
+ function toolSelectionChanged(current: string[], next: string[]): boolean {
170
+ return current.length !== next.length || next.some((tool, index) => tool !== current[index]);
171
+ }
172
+
173
+ function resultOptions(
174
+ options: OmpRenderOptions,
175
+ result: SiftLightToolResult,
176
+ ): OmpRenderOptions & { isError: boolean } {
177
+ return { ...options, isError: result.isError === true };
178
+ }
179
+
180
+ export async function registerOmpSiftLightExtension(
181
+ pi: OmpExtensionAPI,
182
+ searchPolicyAssets = new URL("../plugins/sift-light/hooks/", import.meta.url),
183
+ config?: SiftLightConfig,
184
+ ): Promise<void> {
185
+ const policy = new SearchPolicy(searchPolicyAssets);
186
+ const resolvedConfig =
187
+ config ??
188
+ (await readSiftLightConfigFile(resolveSiftLightConfigPath(ompAgentDir()), {
189
+ missing: process.env[SIFT_LIGHT_CONFIG_ENV]?.trim() ? "error" : "defaults",
190
+ }));
191
+ const semanticJudge = createSemanticJudgeIntegration(
192
+ resolvedConfig.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG,
193
+ );
194
+ const runtime = new SiftLightRuntime(
195
+ new SiftLightService({
196
+ runRipgrep: createRipgrepRunner(),
197
+ structure: createCtagsStructureProvider(),
198
+ semanticJudge,
199
+ }),
200
+ );
201
+ const { locale } = resolvedConfig;
202
+ const enforcement = normalizeSearchEnforcement(
203
+ resolvedConfig.enforceSearch,
204
+ "OMP extension config",
205
+ );
206
+ let selection = Promise.resolve();
207
+ const updateSelection = async (): Promise<void> => {
208
+ const current = pi.getActiveTools();
209
+ const next = selectSearchTools(pi, enforcement === "hard");
210
+ if (toolSelectionChanged(current, next)) await pi.setActiveTools(next);
211
+ };
212
+ const selectTools = (): Promise<void> => {
213
+ selection = selection.then(updateSelection);
214
+ return selection;
215
+ };
216
+
217
+ pi.registerTool({
218
+ name: SIFT_LIGHT_LABEL,
219
+ label: SIFT_LIGHT_LABEL,
220
+ description:
221
+ "Search and navigate code with bounded, verifiable evidence. Use pattern for content or mode=files with query for filenames.",
222
+ approval: "read",
223
+ promptSnippet: "Search file contents without flooding context",
224
+ promptGuidelines: siftLightPromptGuidelines(),
225
+ parameters: siftLightSchema,
226
+
227
+ renderCall(params, _options, theme) {
228
+ return renderSiftLightCall(params, locale, theme);
229
+ },
230
+
231
+ renderResult(result, options, theme) {
232
+ return renderSiftLightResult(result, resultOptions(options, result), locale, theme);
233
+ },
234
+
235
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
236
+ try {
237
+ const result = await runtime.search(
238
+ normalizeOmpToolInput(params),
239
+ ctx.cwd,
240
+ signal,
241
+ resolveContextBudget(ctx.getContextUsage()),
242
+ );
243
+ ctx.ui.setStatus(SESSION_STATUS_KEY, runtime.formatSessionStatus(locale));
244
+ return {
245
+ content: [{ type: "text", text: result.text }],
246
+ details: result.details,
247
+ };
248
+ } catch (error) {
249
+ if (signal?.aborted) throw error;
250
+ runtime.recordFailure();
251
+ ctx.ui.setStatus(SESSION_STATUS_KEY, runtime.formatSessionStatus(locale));
252
+ // oxlint-disable-next-line preserve-caught-error -- the model boundary must not expose a recursive cause chain
253
+ throw new Error(modelErrorText(error));
254
+ }
255
+ },
256
+ });
257
+
258
+ if (enforcement !== "off") {
259
+ pi.on("session_start", selectTools);
260
+ pi.on("before_agent_start", async (event) => {
261
+ await selectTools();
262
+ const guidance = enforcement === "hard" ? SEARCH_POLICY_GUIDANCE : PREFERRED_SEARCH_GUIDANCE;
263
+ return { systemPrompt: [...event.systemPrompt, guidance] };
264
+ });
265
+ if (enforcement === "hard")
266
+ pi.on("tool_call", (event) => policy.check(event.toolName, event.input));
267
+ }
268
+
269
+ pi.on("session_shutdown", async (_event, ctx) => {
270
+ await runtime.shutdown();
271
+ ctx.ui.setStatus(SESSION_STATUS_KEY, undefined);
272
+ });
273
+ }
274
+
275
+ export default async function siftLightOmpExtension(pi: OmpExtensionAPI): Promise<void> {
276
+ await registerOmpSiftLightExtension(pi);
277
+ }
@@ -0,0 +1,7 @@
1
+ import { registerOmpSiftLightExtension } from "./omp-index.js";
2
+
3
+ export default async function marketplaceSiftLightExtension(
4
+ pi: Parameters<typeof registerOmpSiftLightExtension>[0],
5
+ ): Promise<void> {
6
+ await registerOmpSiftLightExtension(pi, new URL("./hooks/", import.meta.url));
7
+ }