sift-light 1.0.3-2 → 1.0.3-4
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/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/hooks/search-policy.mjs +239 -14
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +296 -47
- package/plugins/sift-light/package.json +1 -1
- package/src/evidence-service.ts +3 -3
- package/src/mcp-server.mjs +56 -32
- package/src/path-policy.ts +22 -2
- package/src/prompt-guidelines.ts +1 -1
- package/src/rg.ts +4 -3
- package/src/search-policy-shell.ts +89 -24
- package/src/search-policy-stdin-filter.ts +174 -0
- package/src/search-policy.ts +1 -1
- package/src/service.ts +5 -1
- package/src/workspace-files.ts +2 -1
|
@@ -513,9 +513,9 @@ async function consumeCappedLines(stream, onLine, options = {}) {
|
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
// src/path-policy.ts
|
|
516
|
-
import { realpath } from "fs/promises";
|
|
516
|
+
import { realpath, stat } from "fs/promises";
|
|
517
517
|
import { homedir } from "os";
|
|
518
|
-
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
518
|
+
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
519
519
|
var POSIX_SPECIAL_ROOTS = ["/dev", "/proc", "/sys"];
|
|
520
520
|
var PORTABLE_CREDENTIAL_DIRECTORY_NAMES = [
|
|
521
521
|
".ssh",
|
|
@@ -650,6 +650,28 @@ class SearchPathPolicy {
|
|
|
650
650
|
]);
|
|
651
651
|
return canonical && (!isPathInsideRoot(absolute, this.cwd) || !isPathInsideRoot(canonical, canonicalCwd)) ? canonical : absolute;
|
|
652
652
|
}
|
|
653
|
+
async allowsScopeExpansion(path, scope) {
|
|
654
|
+
if (scope === "strict")
|
|
655
|
+
return false;
|
|
656
|
+
if (scope === "expand")
|
|
657
|
+
return true;
|
|
658
|
+
const searchTarget = await this.resolveSearchTarget(path);
|
|
659
|
+
return isPathInsideRoot(searchTarget, this.cwd);
|
|
660
|
+
}
|
|
661
|
+
async ripgrepWorkingDirectory(searchPath) {
|
|
662
|
+
const absolute = resolve(this.cwd, searchPath);
|
|
663
|
+
if (isPathInsideRoot(absolute, this.cwd))
|
|
664
|
+
return this.cwd;
|
|
665
|
+
let isDirectory;
|
|
666
|
+
try {
|
|
667
|
+
isDirectory = (await stat(absolute)).isDirectory();
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
670
|
+
return this.cwd;
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
return isDirectory ? absolute : dirname(absolute);
|
|
674
|
+
}
|
|
653
675
|
ripgrepGlobArguments(searchPath) {
|
|
654
676
|
const absolute = resolve(this.cwd, searchPath);
|
|
655
677
|
if (isPathInsideRoot(absolute, this.cwd))
|
|
@@ -770,7 +792,7 @@ async function runOwnedProcess(options, consumeOutput) {
|
|
|
770
792
|
|
|
771
793
|
// src/ripgrep-executable.ts
|
|
772
794
|
import { constants } from "fs";
|
|
773
|
-
import { access, stat } from "fs/promises";
|
|
795
|
+
import { access, stat as stat2 } from "fs/promises";
|
|
774
796
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
775
797
|
var OVERRIDE_ENV = "SIFT_LIGHT_RG_PATH";
|
|
776
798
|
var BUNDLED_REPAIR = `Reinstall sift-light with optional dependencies enabled for this platform, or set ${OVERRIDE_ENV} to an absolute ripgrep executable path.`;
|
|
@@ -789,7 +811,7 @@ async function resolveRipgrepExecutable() {
|
|
|
789
811
|
}
|
|
790
812
|
}
|
|
791
813
|
try {
|
|
792
|
-
if (!(await
|
|
814
|
+
if (!(await stat2(executable)).isFile())
|
|
793
815
|
throw new Error("Expected an executable file");
|
|
794
816
|
await access(executable, constants.X_OK);
|
|
795
817
|
} catch (cause) {
|
|
@@ -973,12 +995,12 @@ function describeUnreadableDiagnostics(diagnostics) {
|
|
|
973
995
|
}
|
|
974
996
|
|
|
975
997
|
// src/source.ts
|
|
976
|
-
import { readFile as readFile2, realpath as realpath2, stat as
|
|
998
|
+
import { readFile as readFile2, realpath as realpath2, stat as stat3 } from "fs/promises";
|
|
977
999
|
var SOURCE_RANGE_METADATA_RESERVE_BYTES = 1024;
|
|
978
1000
|
var MAX_SOURCE_RANGE_BYTES = MAX_RESULT_BYTES - SOURCE_RANGE_METADATA_RESERVE_BYTES;
|
|
979
1001
|
async function getSourceRevision(path, onError) {
|
|
980
1002
|
try {
|
|
981
|
-
const metadata2 = await
|
|
1003
|
+
const metadata2 = await stat3(path);
|
|
982
1004
|
return sourceRevisionFromStats(metadata2);
|
|
983
1005
|
} catch (error) {
|
|
984
1006
|
onError?.(error);
|
|
@@ -1439,6 +1461,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1439
1461
|
const searchPath = resolve4(cwd, request.path ?? ".");
|
|
1440
1462
|
const policy = new SearchPathPolicy(cwd);
|
|
1441
1463
|
const validatedSearchPath = await policy.resolveSearchTarget(searchPath);
|
|
1464
|
+
const ripgrepCwd = await policy.ripgrepWorkingDirectory(validatedSearchPath);
|
|
1442
1465
|
const expectedSearchTarget = await policy.resolveExistingPath(validatedSearchPath);
|
|
1443
1466
|
const searchTarget = isPathInsideCwd(validatedSearchPath, cwd) ? relative2(resolve4(cwd), validatedSearchPath) || "." : validatedSearchPath;
|
|
1444
1467
|
const args2 = buildRipgrepArguments(request, cwd, validatedSearchPath);
|
|
@@ -1528,7 +1551,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1528
1551
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
1529
1552
|
"--",
|
|
1530
1553
|
searchTarget
|
|
1531
|
-
],
|
|
1554
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
1532
1555
|
let ignoredFileCount = 0;
|
|
1533
1556
|
let ignoredFileSamples = [];
|
|
1534
1557
|
let filesystemCoverage = "complete";
|
|
@@ -1550,7 +1573,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1550
1573
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
1551
1574
|
"--",
|
|
1552
1575
|
searchTarget
|
|
1553
|
-
],
|
|
1576
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
1554
1577
|
const ignored = [...allCandidates.revisions.keys()].filter((path) => !before.revisions.has(path));
|
|
1555
1578
|
ignoredFileCount = ignored.length;
|
|
1556
1579
|
ignoredFileSamples = ignored.slice(0, 20).map((path) => displayPath(path, cwd).displayPath);
|
|
@@ -1579,7 +1602,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1579
1602
|
if (before.unreadable.length > 0)
|
|
1580
1603
|
filesystemCoverageReasons.add(describeUnreadableDiagnostics(before.unreadable));
|
|
1581
1604
|
await assertSearchTargetIdentity(policy, validatedSearchPath, expectedSearchTarget);
|
|
1582
|
-
const { code, stderr } = await runOwnedProcess({ executable, args: args2, cwd, ...signal ? { signal } : {} }, (stdout) => consumeCappedLines(stdout, onLine, {
|
|
1605
|
+
const { code, stderr } = await runOwnedProcess({ executable, args: args2, cwd: ripgrepCwd, ...signal ? { signal } : {} }, (stdout) => consumeCappedLines(stdout, onLine, {
|
|
1583
1606
|
maxLineBytes: maxEventBytes,
|
|
1584
1607
|
onLineTooLong: ({ prefix, observedBytes }) => {
|
|
1585
1608
|
const source = oversizedMatchPath(prefix, cwd);
|
|
@@ -1822,7 +1845,7 @@ function createCtagsStructureProvider(options = {}) {
|
|
|
1822
1845
|
// package.json
|
|
1823
1846
|
var package_default = {
|
|
1824
1847
|
name: "sift-light",
|
|
1825
|
-
version: "1.0.3-
|
|
1848
|
+
version: "1.0.3-4",
|
|
1826
1849
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
1827
1850
|
keywords: [
|
|
1828
1851
|
"ai-agent",
|
|
@@ -2179,7 +2202,7 @@ class OwnedTaskQueue {
|
|
|
2179
2202
|
}
|
|
2180
2203
|
|
|
2181
2204
|
// src/concept-search.ts
|
|
2182
|
-
import { dirname as
|
|
2205
|
+
import { dirname as dirname4, join as join5, resolve as resolve12 } from "path";
|
|
2183
2206
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2184
2207
|
import { StringDecoder } from "string_decoder";
|
|
2185
2208
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
@@ -2343,7 +2366,7 @@ import { extname as extname2, resolve as resolve11 } from "path";
|
|
|
2343
2366
|
import { lstat, mkdir, mkdtemp, open, rm, writeFile } from "fs/promises";
|
|
2344
2367
|
import { constants as constants2 } from "fs";
|
|
2345
2368
|
import { tmpdir } from "os";
|
|
2346
|
-
import { dirname, join as join4, parse, relative as relative4, resolve as resolve8 } from "path";
|
|
2369
|
+
import { dirname as dirname2, join as join4, parse, relative as relative4, resolve as resolve8 } from "path";
|
|
2347
2370
|
|
|
2348
2371
|
// src/workspace-files.ts
|
|
2349
2372
|
import { relative as relative3, resolve as resolve7, sep as sep2 } from "path";
|
|
@@ -2361,6 +2384,7 @@ async function listWorkspaceFiles(cwd, signal, options = {}) {
|
|
|
2361
2384
|
const absolutePath = resolve7(cwd, options.path ?? ".");
|
|
2362
2385
|
const policy = new SearchPathPolicy(cwd);
|
|
2363
2386
|
const searchPath = await policy.resolveSearchTarget(absolutePath);
|
|
2387
|
+
const ripgrepCwd = await policy.ripgrepWorkingDirectory(searchPath);
|
|
2364
2388
|
const maxFiles = options.maxFiles ?? MAX_SOURCE_REVISION_FILES;
|
|
2365
2389
|
if (!Number.isSafeInteger(maxFiles) || maxFiles < 1)
|
|
2366
2390
|
throw new SiftLightError("Candidate file limit must be a positive integer");
|
|
@@ -2386,7 +2410,7 @@ async function listWorkspaceFiles(cwd, signal, options = {}) {
|
|
|
2386
2410
|
"--",
|
|
2387
2411
|
searchPath
|
|
2388
2412
|
],
|
|
2389
|
-
cwd,
|
|
2413
|
+
cwd: ripgrepCwd,
|
|
2390
2414
|
...signal ? { signal } : {}
|
|
2391
2415
|
}, async (stdout) => {
|
|
2392
2416
|
let pending = Buffer.alloc(0);
|
|
@@ -2454,11 +2478,11 @@ function partitionPaths(paths) {
|
|
|
2454
2478
|
}
|
|
2455
2479
|
function relevantDirectories(cwd, paths) {
|
|
2456
2480
|
const directories = new Set;
|
|
2457
|
-
for (const path of [cwd, ...paths.map((sourcePath) =>
|
|
2481
|
+
for (const path of [cwd, ...paths.map((sourcePath) => dirname2(resolve8(cwd, sourcePath)))]) {
|
|
2458
2482
|
let current = path;
|
|
2459
2483
|
for (;; ) {
|
|
2460
2484
|
directories.add(current);
|
|
2461
|
-
const parent =
|
|
2485
|
+
const parent = dirname2(current);
|
|
2462
2486
|
if (current === parent)
|
|
2463
2487
|
break;
|
|
2464
2488
|
current = parent;
|
|
@@ -2542,12 +2566,12 @@ async function filterHistoricalPaths(cwd, paths, request, signal) {
|
|
|
2542
2566
|
for (const path of group) {
|
|
2543
2567
|
const safe = workspaceRelativePath(absoluteCwd, path);
|
|
2544
2568
|
const placeholder = resolve8(target, safe);
|
|
2545
|
-
await mkdir(
|
|
2569
|
+
await mkdir(dirname2(placeholder), { recursive: true });
|
|
2546
2570
|
await writeFile(placeholder, "");
|
|
2547
2571
|
}
|
|
2548
2572
|
for (const ignore of ignoreFiles) {
|
|
2549
2573
|
const destination = join4(tree, ignore.local);
|
|
2550
|
-
await mkdir(
|
|
2574
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2551
2575
|
await writeFile(destination, ignore.bytes);
|
|
2552
2576
|
}
|
|
2553
2577
|
const privacy = await listWorkspaceFiles(tree, signal, { ignoreParents: false });
|
|
@@ -3540,7 +3564,7 @@ async function readWorkspaceDocument(path, cwd, signal, expected, readBudget = M
|
|
|
3540
3564
|
}
|
|
3541
3565
|
|
|
3542
3566
|
// src/syntax.ts
|
|
3543
|
-
import { dirname as
|
|
3567
|
+
import { dirname as dirname3 } from "path";
|
|
3544
3568
|
import { fileURLToPath } from "url";
|
|
3545
3569
|
|
|
3546
3570
|
// src/syntax-tree.ts
|
|
@@ -4242,7 +4266,7 @@ async function parseSyntax(path, text, signal, pattern) {
|
|
|
4242
4266
|
const result = await runOwnedProcess({
|
|
4243
4267
|
executable: process.execPath,
|
|
4244
4268
|
args: args2,
|
|
4245
|
-
cwd:
|
|
4269
|
+
cwd: dirname3(worker),
|
|
4246
4270
|
env,
|
|
4247
4271
|
signal: controller.signal,
|
|
4248
4272
|
input: Buffer.from(JSON.stringify({ language, text, pattern }))
|
|
@@ -4761,7 +4785,7 @@ async function similarities(query, passages, parent, onProgress) {
|
|
|
4761
4785
|
worker,
|
|
4762
4786
|
"--infer"
|
|
4763
4787
|
] : [worker, "--infer"],
|
|
4764
|
-
cwd:
|
|
4788
|
+
cwd: dirname4(worker),
|
|
4765
4789
|
env: { ...env, SIFT_LIGHT_CONCEPT_CACHE_STAGING_DIR: stagingRoot },
|
|
4766
4790
|
...parent ? { signal: parent } : {},
|
|
4767
4791
|
input: Buffer.from(JSON.stringify({
|
|
@@ -5183,7 +5207,7 @@ async function structuralSearch(input, access) {
|
|
|
5183
5207
|
}
|
|
5184
5208
|
|
|
5185
5209
|
// src/evidence-service.ts
|
|
5186
|
-
import { dirname as
|
|
5210
|
+
import { dirname as dirname5, extname as extname3, resolve as resolve21 } from "path";
|
|
5187
5211
|
|
|
5188
5212
|
// src/analysis-store.ts
|
|
5189
5213
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -8440,7 +8464,7 @@ async function continueSource(cursor, access, continuations, expectedPath) {
|
|
|
8440
8464
|
}
|
|
8441
8465
|
|
|
8442
8466
|
// src/evidence-validation.ts
|
|
8443
|
-
import { realpath as realpath5, stat as
|
|
8467
|
+
import { realpath as realpath5, stat as stat4 } from "fs/promises";
|
|
8444
8468
|
import { resolve as resolve19 } from "path";
|
|
8445
8469
|
|
|
8446
8470
|
// src/evidence-validity.ts
|
|
@@ -8478,9 +8502,9 @@ async function confirmWorktreeState(path, cwd, signal) {
|
|
|
8478
8502
|
const policy = new SearchPathPolicy(cwd);
|
|
8479
8503
|
try {
|
|
8480
8504
|
policy.assertPath(absolute);
|
|
8481
|
-
const before = await
|
|
8505
|
+
const before = await stat4(absolute);
|
|
8482
8506
|
const beforeCanonical = await realpath5(absolute);
|
|
8483
|
-
const after = await
|
|
8507
|
+
const after = await stat4(absolute);
|
|
8484
8508
|
const afterCanonical = await realpath5(absolute);
|
|
8485
8509
|
if (!before.isFile() || !after.isFile()) {
|
|
8486
8510
|
return { status: "stale", reason: "Source is no longer a regular file" };
|
|
@@ -8622,9 +8646,9 @@ async function validateSnapshotTarget(target, cwd, policy, signal) {
|
|
|
8622
8646
|
const result = await confirmWorktreeState(target.path, cwd, signal);
|
|
8623
8647
|
return { path: target.path, role: "source", ...result };
|
|
8624
8648
|
}
|
|
8625
|
-
const before = await
|
|
8649
|
+
const before = await stat4(absolute);
|
|
8626
8650
|
const beforeCanonical = canonical;
|
|
8627
|
-
const after = await
|
|
8651
|
+
const after = await stat4(absolute);
|
|
8628
8652
|
const afterCanonical = await realpath5(absolute);
|
|
8629
8653
|
if (!before.isFile() || !after.isFile()) {
|
|
8630
8654
|
return {
|
|
@@ -10951,10 +10975,10 @@ function searchScope(request) {
|
|
|
10951
10975
|
}
|
|
10952
10976
|
async function navigationRoot(cwd, path, signal) {
|
|
10953
10977
|
const absolute = resolve21(cwd, path);
|
|
10954
|
-
const repository = await findGitRepository(
|
|
10978
|
+
const repository = await findGitRepository(dirname5(absolute), signal);
|
|
10955
10979
|
if (repository)
|
|
10956
10980
|
return repository;
|
|
10957
|
-
return isPathInsideCwd(absolute, cwd) ? resolve21(cwd) :
|
|
10981
|
+
return isPathInsideCwd(absolute, cwd) ? resolve21(cwd) : dirname5(absolute);
|
|
10958
10982
|
}
|
|
10959
10983
|
function navigationFilters(input) {
|
|
10960
10984
|
const request = normalizeRequest({
|
|
@@ -11069,7 +11093,7 @@ class EvidenceService {
|
|
|
11069
11093
|
maxFiles: access.maxFiles
|
|
11070
11094
|
});
|
|
11071
11095
|
const candidates = await collect(request);
|
|
11072
|
-
if (input.changes || request.
|
|
11096
|
+
if (input.changes || request.path === undefined || !await new SearchPathPolicy(access.cwd).allowsScopeExpansion(request.path, request.scope) || candidates.files.length > 0 || candidates.partial) {
|
|
11073
11097
|
return { candidates, request };
|
|
11074
11098
|
}
|
|
11075
11099
|
const { path: requestedPath, ...projectRequest } = request;
|
|
@@ -11214,7 +11238,7 @@ class EvidenceService {
|
|
|
11214
11238
|
});
|
|
11215
11239
|
}, signal);
|
|
11216
11240
|
let chunkResults = await runChunks();
|
|
11217
|
-
if (!input.changes && input.path !== undefined && input.scope
|
|
11241
|
+
if (!input.changes && input.path !== undefined && await new SearchPathPolicy(cwd).allowsScopeExpansion(input.path, input.scope) && chunkResults.every(({ candidates }) => !candidates.partial && candidates.files.length === 0)) {
|
|
11218
11242
|
chunkResults = await runChunks(input.path.replace(/^@/, ""));
|
|
11219
11243
|
}
|
|
11220
11244
|
const reasons = new Set;
|
|
@@ -13383,7 +13407,7 @@ class SiftLightService {
|
|
|
13383
13407
|
throw new SiftLightError("line requires mode=inspect");
|
|
13384
13408
|
const request = normalizeRequest(input);
|
|
13385
13409
|
let scan = await this.#runRipgrep(request, cwd, signal);
|
|
13386
|
-
if (scan.totalMatches === 0 && request.path !== undefined && request.scope
|
|
13410
|
+
if (scan.totalMatches === 0 && request.path !== undefined && await new SearchPathPolicy(cwd).allowsScopeExpansion(request.path, request.scope)) {
|
|
13387
13411
|
const { path: requestedPath, ...projectRequest } = request;
|
|
13388
13412
|
scan = await this.#runRipgrep({ ...projectRequest, expandedFromPath: requestedPath }, cwd, signal);
|
|
13389
13413
|
}
|
|
@@ -13653,7 +13677,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13653
13677
|
return [
|
|
13654
13678
|
SIFT_LIGHT_DISCOVERY_GUIDANCE,
|
|
13655
13679
|
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
13656
|
-
`An omitted path searches the project cwd. Use scope:"strict"
|
|
13680
|
+
`An omitted path searches the project cwd. Use scope:"strict" to disable zero-match expansion; by default, explicit paths inside cwd that return no matches retry from cwd and report project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd; those external targets stay isolated unless scope:"expand" opts into the same cwd retry. Protected external system areas and .git internals remain excluded. Git changes mode remains cwd-scoped.`,
|
|
13657
13681
|
`Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
|
|
13658
13682
|
`Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
|
|
13659
13683
|
`Mode="inspect" with a direct path/line or ordinary retained match returns bounded source windows and source-revision metadata. Some retained analysis selectors return only revision metadata; use direct path/line or the host read capability when source is needed. Do not use inspection merely to obtain a citation.`,
|
|
@@ -20434,9 +20458,182 @@ function classifyCommand(argv, language, depth = 0) {
|
|
|
20434
20458
|
return classifyCommand(args2.slice(start2), language, depth + 1);
|
|
20435
20459
|
}
|
|
20436
20460
|
|
|
20461
|
+
// src/search-policy-stdin-filter.ts
|
|
20462
|
+
var GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
|
|
20463
|
+
var GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
|
|
20464
|
+
var GREP_LONG_SWITCHES = new Set([
|
|
20465
|
+
"--basic-regexp",
|
|
20466
|
+
"--extended-regexp",
|
|
20467
|
+
"--fixed-strings",
|
|
20468
|
+
"--perl-regexp",
|
|
20469
|
+
"--ignore-case",
|
|
20470
|
+
"--no-ignore-case",
|
|
20471
|
+
"--invert-match",
|
|
20472
|
+
"--word-regexp",
|
|
20473
|
+
"--line-regexp",
|
|
20474
|
+
"--count",
|
|
20475
|
+
"--files-with-matches",
|
|
20476
|
+
"--files-without-match",
|
|
20477
|
+
"--only-matching",
|
|
20478
|
+
"--quiet",
|
|
20479
|
+
"--silent",
|
|
20480
|
+
"--no-messages",
|
|
20481
|
+
"--byte-offset",
|
|
20482
|
+
"--line-number",
|
|
20483
|
+
"--with-filename",
|
|
20484
|
+
"--no-filename",
|
|
20485
|
+
"--initial-tab",
|
|
20486
|
+
"--null",
|
|
20487
|
+
"--null-data",
|
|
20488
|
+
"--text",
|
|
20489
|
+
"--binary",
|
|
20490
|
+
"--line-buffered",
|
|
20491
|
+
"--no-group-separator"
|
|
20492
|
+
]);
|
|
20493
|
+
var GREP_LONG_VALUES = new Set([
|
|
20494
|
+
"--after-context",
|
|
20495
|
+
"--before-context",
|
|
20496
|
+
"--context",
|
|
20497
|
+
"--max-count",
|
|
20498
|
+
"--binary-files",
|
|
20499
|
+
"--label",
|
|
20500
|
+
"--group-separator",
|
|
20501
|
+
"--include",
|
|
20502
|
+
"--exclude",
|
|
20503
|
+
"--exclude-dir"
|
|
20504
|
+
]);
|
|
20505
|
+
function isStdinOnlyGrep(args2) {
|
|
20506
|
+
const positionals = [];
|
|
20507
|
+
let hasPatternOption = false;
|
|
20508
|
+
let optionsEnded = false;
|
|
20509
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
20510
|
+
const arg = args2[index];
|
|
20511
|
+
if (arg === null || arg === undefined)
|
|
20512
|
+
return false;
|
|
20513
|
+
if (positionals.length > 0 && arg !== "-")
|
|
20514
|
+
return false;
|
|
20515
|
+
if (!optionsEnded && arg === "--") {
|
|
20516
|
+
optionsEnded = true;
|
|
20517
|
+
continue;
|
|
20518
|
+
}
|
|
20519
|
+
if (!optionsEnded && arg.startsWith("--")) {
|
|
20520
|
+
const separator = arg.indexOf("=");
|
|
20521
|
+
const option = separator < 0 ? arg : arg.slice(0, separator);
|
|
20522
|
+
const attached = separator < 0 ? undefined : arg.slice(separator + 1);
|
|
20523
|
+
if (option === "--regexp") {
|
|
20524
|
+
hasPatternOption = true;
|
|
20525
|
+
} else if (option === "--color" || option === "--colour") {
|
|
20526
|
+
continue;
|
|
20527
|
+
} else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
|
|
20528
|
+
continue;
|
|
20529
|
+
} else if (!GREP_LONG_VALUES.has(option)) {
|
|
20530
|
+
return false;
|
|
20531
|
+
}
|
|
20532
|
+
if (attached === undefined) {
|
|
20533
|
+
index += 1;
|
|
20534
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20535
|
+
return false;
|
|
20536
|
+
}
|
|
20537
|
+
continue;
|
|
20538
|
+
}
|
|
20539
|
+
if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
|
|
20540
|
+
for (let optionIndex = 1;optionIndex < arg.length; optionIndex += 1) {
|
|
20541
|
+
const option = arg[optionIndex];
|
|
20542
|
+
if (option === "e") {
|
|
20543
|
+
hasPatternOption = true;
|
|
20544
|
+
} else if (option !== undefined && GREP_SWITCHES.has(option)) {
|
|
20545
|
+
continue;
|
|
20546
|
+
} else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
|
|
20547
|
+
return false;
|
|
20548
|
+
}
|
|
20549
|
+
if (optionIndex === arg.length - 1) {
|
|
20550
|
+
index += 1;
|
|
20551
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20552
|
+
return false;
|
|
20553
|
+
}
|
|
20554
|
+
break;
|
|
20555
|
+
}
|
|
20556
|
+
continue;
|
|
20557
|
+
}
|
|
20558
|
+
positionals.push(arg);
|
|
20559
|
+
}
|
|
20560
|
+
return hasPatternOption ? positionals.every((operand) => operand === "-") : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
|
|
20561
|
+
}
|
|
20562
|
+
var SELECT_STRING_SWITCHES = new Set([
|
|
20563
|
+
"simplematch",
|
|
20564
|
+
"casesensitive",
|
|
20565
|
+
"quiet",
|
|
20566
|
+
"list",
|
|
20567
|
+
"noemphasis",
|
|
20568
|
+
"notmatch",
|
|
20569
|
+
"allmatches",
|
|
20570
|
+
"raw",
|
|
20571
|
+
"verbose",
|
|
20572
|
+
"debug"
|
|
20573
|
+
]);
|
|
20574
|
+
var SELECT_STRING_VALUES = new Set([
|
|
20575
|
+
"culture",
|
|
20576
|
+
"encoding",
|
|
20577
|
+
"context",
|
|
20578
|
+
"erroraction",
|
|
20579
|
+
"warningaction",
|
|
20580
|
+
"informationaction",
|
|
20581
|
+
"outvariable",
|
|
20582
|
+
"pipelinevariable"
|
|
20583
|
+
]);
|
|
20584
|
+
function isPipelineOnlySelectString(args2, namedParameters) {
|
|
20585
|
+
let hasPattern = false;
|
|
20586
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
20587
|
+
const arg = args2[index];
|
|
20588
|
+
if (arg === null || arg === undefined)
|
|
20589
|
+
return false;
|
|
20590
|
+
if (!namedParameters[index]) {
|
|
20591
|
+
if (hasPattern)
|
|
20592
|
+
return false;
|
|
20593
|
+
hasPattern = true;
|
|
20594
|
+
continue;
|
|
20595
|
+
}
|
|
20596
|
+
const separator = arg.indexOf(":");
|
|
20597
|
+
const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
|
|
20598
|
+
if (SELECT_STRING_SWITCHES.has(option) && separator < 0)
|
|
20599
|
+
continue;
|
|
20600
|
+
if (option !== "pattern" && !SELECT_STRING_VALUES.has(option))
|
|
20601
|
+
return false;
|
|
20602
|
+
if (option === "pattern") {
|
|
20603
|
+
if (hasPattern)
|
|
20604
|
+
return false;
|
|
20605
|
+
hasPattern = true;
|
|
20606
|
+
}
|
|
20607
|
+
if (separator < 0) {
|
|
20608
|
+
index += 1;
|
|
20609
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20610
|
+
return false;
|
|
20611
|
+
}
|
|
20612
|
+
}
|
|
20613
|
+
return hasPattern;
|
|
20614
|
+
}
|
|
20615
|
+
function isStdinOnlySearchFilter(words, language, namedParameters) {
|
|
20616
|
+
const executable = words[0];
|
|
20617
|
+
if (executable === null || executable === undefined)
|
|
20618
|
+
return false;
|
|
20619
|
+
const name2 = executableName(executable);
|
|
20620
|
+
if (language === "powershell")
|
|
20621
|
+
return ["select-string", "sls"].includes(name2.toLowerCase()) && isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1));
|
|
20622
|
+
return ["grep", "egrep", "fgrep"].includes(name2) && isStdinOnlyGrep(words.slice(1));
|
|
20623
|
+
}
|
|
20624
|
+
|
|
20437
20625
|
// src/search-policy-shell.ts
|
|
20438
20626
|
var MAX_POLICY_COMMAND_BYTES = 64 * 1024;
|
|
20439
20627
|
var MAX_SHELL_NESTING = 4;
|
|
20628
|
+
var POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
|
|
20629
|
+
"get-item",
|
|
20630
|
+
"gi",
|
|
20631
|
+
"get-childitem",
|
|
20632
|
+
"gci",
|
|
20633
|
+
"dir",
|
|
20634
|
+
"ls"
|
|
20635
|
+
]);
|
|
20636
|
+
var POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
|
|
20440
20637
|
function decodeBashDoubleQuoted(value) {
|
|
20441
20638
|
let result = "";
|
|
20442
20639
|
for (let index = 0;index < value.length; index += 1) {
|
|
@@ -20522,6 +20719,18 @@ function commandWordNodes(node, language) {
|
|
|
20522
20719
|
function commandWords(node, language) {
|
|
20523
20720
|
return commandWordNodes(node, language).map((word) => literalWord(word, language));
|
|
20524
20721
|
}
|
|
20722
|
+
function hasBashWordExpansion(text) {
|
|
20723
|
+
if (text.startsWith("~") || text.startsWith("="))
|
|
20724
|
+
return true;
|
|
20725
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
20726
|
+
if (text[index] === "\\") {
|
|
20727
|
+
index += 1;
|
|
20728
|
+
} else if ("*?[]{}".includes(text[index] ?? "")) {
|
|
20729
|
+
return true;
|
|
20730
|
+
}
|
|
20731
|
+
}
|
|
20732
|
+
return false;
|
|
20733
|
+
}
|
|
20525
20734
|
function hasUntranslatedShellSyntax(node, language) {
|
|
20526
20735
|
const text = node.text;
|
|
20527
20736
|
if (language === "powershell") {
|
|
@@ -20534,30 +20743,70 @@ function hasUntranslatedShellSyntax(node, language) {
|
|
|
20534
20743
|
if (["raw_string", "string", "string_content"].includes(node.type))
|
|
20535
20744
|
return false;
|
|
20536
20745
|
if (["word", "number", "command_name"].includes(node.type))
|
|
20537
|
-
return
|
|
20746
|
+
return hasBashWordExpansion(text);
|
|
20538
20747
|
if (node.type === "concatenation")
|
|
20539
20748
|
return node.namedChildren.some((child) => child !== null && hasUntranslatedShellSyntax(child, language));
|
|
20540
20749
|
return false;
|
|
20541
20750
|
}
|
|
20751
|
+
function hasInputRedirectNodes(owner) {
|
|
20752
|
+
return owner.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"]).some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
|
|
20753
|
+
}
|
|
20754
|
+
function hasInputRedirect(node) {
|
|
20755
|
+
if (hasInputRedirectNodes(node))
|
|
20756
|
+
return true;
|
|
20757
|
+
for (let parent = node.parent;parent; parent = parent.parent) {
|
|
20758
|
+
if (parent.type !== "redirected_statement")
|
|
20759
|
+
continue;
|
|
20760
|
+
const body2 = parent.childForFieldName("body");
|
|
20761
|
+
const last = body2?.descendantsOfType("command").at(-1) ?? (body2?.type === "command" ? body2 : undefined);
|
|
20762
|
+
if (last?.startIndex === node.startIndex && last.endIndex === node.endIndex && parent.childrenForFieldName("redirect").some((redirect) => redirect !== null && hasInputRedirectNodes(redirect)))
|
|
20763
|
+
return true;
|
|
20764
|
+
}
|
|
20765
|
+
return false;
|
|
20766
|
+
}
|
|
20542
20767
|
function isSafePipelineFilter(node, language, words) {
|
|
20543
|
-
const
|
|
20544
|
-
if (
|
|
20768
|
+
const wordNodes = commandWordNodes(node, language);
|
|
20769
|
+
if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
|
|
20545
20770
|
return false;
|
|
20546
|
-
|
|
20547
|
-
|
|
20548
|
-
if (
|
|
20771
|
+
if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language)))
|
|
20772
|
+
return false;
|
|
20773
|
+
if (hasInputRedirect(node))
|
|
20549
20774
|
return false;
|
|
20550
20775
|
const pipeline = node.parent;
|
|
20551
|
-
if (!pipeline || pipeline
|
|
20776
|
+
if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
|
|
20552
20777
|
return false;
|
|
20553
|
-
const
|
|
20554
|
-
const
|
|
20555
|
-
if (
|
|
20778
|
+
const stages = pipeline.namedChildren.filter((child) => child !== null);
|
|
20779
|
+
const position = stages.findIndex((stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex);
|
|
20780
|
+
if (position < 1)
|
|
20556
20781
|
return false;
|
|
20557
|
-
|
|
20558
|
-
|
|
20559
|
-
|
|
20560
|
-
|
|
20782
|
+
let fileObjectFlow = false;
|
|
20783
|
+
for (const [index, stage] of stages.entries()) {
|
|
20784
|
+
if (index === position)
|
|
20785
|
+
continue;
|
|
20786
|
+
const commands = stage.descendantsOfType("command");
|
|
20787
|
+
for (const candidate of commands) {
|
|
20788
|
+
if (candidate === null)
|
|
20789
|
+
continue;
|
|
20790
|
+
const candidateWords = commandWords(candidate, language);
|
|
20791
|
+
const decision = classifyCommand(candidateWords, language);
|
|
20792
|
+
if (decision.kind || decision.nested)
|
|
20793
|
+
return false;
|
|
20794
|
+
if (language !== "powershell" || index > position)
|
|
20795
|
+
continue;
|
|
20796
|
+
const executable = candidateWords[0];
|
|
20797
|
+
if (executable === null || executable === undefined)
|
|
20798
|
+
continue;
|
|
20799
|
+
const name2 = executableName(executable).toLowerCase();
|
|
20800
|
+
if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name2))
|
|
20801
|
+
fileObjectFlow = true;
|
|
20802
|
+
}
|
|
20803
|
+
if (language === "powershell" && index < position && stage.type === "command") {
|
|
20804
|
+
const executable = commandWords(stage, language)[0];
|
|
20805
|
+
if (executable !== null && executable !== undefined && POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase()))
|
|
20806
|
+
fileObjectFlow = false;
|
|
20807
|
+
}
|
|
20808
|
+
}
|
|
20809
|
+
return !fileObjectFlow;
|
|
20561
20810
|
}
|
|
20562
20811
|
|
|
20563
20812
|
class ShellSearchPolicy {
|
|
@@ -20639,7 +20888,7 @@ class ShellSearchPolicy {
|
|
|
20639
20888
|
}
|
|
20640
20889
|
|
|
20641
20890
|
// src/search-policy.ts
|
|
20642
|
-
var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at
|
|
20891
|
+
var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
|
|
20643
20892
|
var PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
|
|
20644
20893
|
var PREFERRED_SEARCH_GUIDANCE = "Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";
|
|
20645
20894
|
var contentTools = new Set(["grep", "Grep", "SearchFileContent"]);
|
package/src/evidence-service.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { AnalysisStore } from "./analysis-store.js";
|
|
|
10
10
|
import type { AnalysisItem, AnalysisResultSet } from "./analysis-types.js";
|
|
11
11
|
import { abortError, CursorError, SiftLightError } from "./errors.js";
|
|
12
12
|
import { findGitRepository } from "./git-repository.js";
|
|
13
|
-
import { isPathInsideCwd, isPathInsideRoot } from "./path-policy.js";
|
|
13
|
+
import { isPathInsideCwd, isPathInsideRoot, SearchPathPolicy } from "./path-policy.js";
|
|
14
14
|
import { resolveInspectionTarget } from "./inspect.js";
|
|
15
15
|
import {
|
|
16
16
|
collectEvidenceCandidates,
|
|
@@ -457,8 +457,8 @@ export class EvidenceService {
|
|
|
457
457
|
const candidates = await collect(request);
|
|
458
458
|
if (
|
|
459
459
|
input.changes ||
|
|
460
|
-
request.scope === "strict" ||
|
|
461
460
|
request.path === undefined ||
|
|
461
|
+
!(await new SearchPathPolicy(access.cwd).allowsScopeExpansion(request.path, request.scope)) ||
|
|
462
462
|
candidates.files.length > 0 ||
|
|
463
463
|
candidates.partial
|
|
464
464
|
) {
|
|
@@ -649,7 +649,7 @@ export class EvidenceService {
|
|
|
649
649
|
if (
|
|
650
650
|
!input.changes &&
|
|
651
651
|
input.path !== undefined &&
|
|
652
|
-
input.scope
|
|
652
|
+
(await new SearchPathPolicy(cwd).allowsScopeExpansion(input.path, input.scope)) &&
|
|
653
653
|
chunkResults.every(({ candidates }) => !candidates.partial && candidates.files.length === 0)
|
|
654
654
|
) {
|
|
655
655
|
chunkResults = await runChunks(input.path.replace(/^@/, ""));
|