mobile-context-trimmer 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isonka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # mobile-context-trimmer
2
+
3
+ TypeScript CLI and library that scans native **iOS** and **Android** repositories and builds a token-budgeted markdown context bundle for LLMs.
4
+
5
+ ## Why this exists
6
+
7
+ Mobile codebases mix Swift/Kotlin sources with Xcode and Gradle noise. Plain “scan everything” bundles waste budget on generated or churn-heavy files, while missing the files that actually changed recently. This tool applies **mobile-aware defaults** (extensions, ignores) and ranks files using **TF‑IDF-style relevance**, **git recency with Xcode-safe rules**, and **file-type priority**—similar to `context-trimmer`, but tuned for native apps.
8
+
9
+ ## How ranking works
10
+
11
+ Each file gets a weighted score from three signals:
12
+
13
+ | Signal | Description |
14
+ | --- | --- |
15
+ | **Query match** | TF × IDF over query terms in file content (corpus = scanned files). Rare terms that appear in few files count more than words that appear everywhere. |
16
+ | **Recency** | Last commit time from batched `git log` (first-seen path wins), normalized across the set. **Xcode-safe:** paths under `.xcodeproj/`, `*.pbxproj`, and `xcuserdata` **do not** use git timestamps (they are excluded from the git map and use **filesystem `mtime`** instead), so Xcode constantly rewriting the project file does not drown out real Swift/Kotlin activity. |
17
+ | **File type** | Extension weights (e.g. `.swift` / `.kt` favored over `.xml` / `.properties`). |
18
+
19
+ Default weights: keyword **0.55**, recency **0.30**, type **0.15** (recency matters more on mobile than in generic JS repos).
20
+
21
+ Files are then selected in **rank order** until the token budget is exhausted. When you pass a **non-empty `--query`**, the bundler applies a default **keyword relevance floor** (`minKeywordScore: 0`): files with **no lexical match** to the query (`keywordScore <= 0`) are **dropped** before budgeting, so recently touched A/B experiment stubs cannot crowd out on-topic files. Raise the floor with `--min-keyword-score` to demand stronger matches; use **`--min-keyword-score -1`** to disable the floor while keeping ranking.
22
+
23
+ **Tail noise from recency:** Utilities can clear the keyword floor from a **spurious** hit (e.g. `subscriptions` as a property name) while **recency still applies at full strength**, so combined scores can stay high (~0.3–0.45) and a **low** `--min-combined-score` (near 0) will not remove them.
24
+
25
+ Two levers work together:
26
+
27
+ 1. **Recency dampening** (on by default when `--query` is non-empty): recency is scaled by `min(1, keywordScore / reference)` with default reference **~0.055** (`DEFAULT_KEYWORD_RECENCY_REFERENCE`). Weak lexical matches no longer inherit the full 30% recency term. **`--keyword-recency-reference 0`** restores legacy behavior (no dampening). Lower the reference to dampen more aggressively.
28
+
29
+ 2. **Combined score floor:** With a query, the CLI now defaults **`--min-combined-score` to `0.1`** (`DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY`). Calibrate on your repo—many iOS codebases land in roughly the **0.08–0.15** band for separating boilerplate tails from task files; raise toward **0.2** if utilities still slip through. **`--min-combined-score -1`** disables the floor.
30
+
31
+ ## Quick start
32
+
33
+ ```bash
34
+ npx mobile-context-trimmer --dir ./MyApp --query "fix login crash" --budget 32000 --out mobile-context.md
35
+ ```
36
+
37
+ Stricter tail (defaults already apply dampening + combined floor `0.1`; raise if needed):
38
+
39
+ ```bash
40
+ npx mobile-context-trimmer --dir ./MyApp --query "add subscriptions to menu" --min-combined-score 0.15 --budget 32000 --out mobile-context.md
41
+ ```
42
+
43
+ Stream to stdout (omit `--out`):
44
+
45
+ ```bash
46
+ npx mobile-context-trimmer --dir ./MyApp --query "navigation stack" --budget 16000
47
+ ```
48
+
49
+ ## CLI options
50
+
51
+ | Option | Type | Default | Description |
52
+ | --- | --- | --- | --- |
53
+ | `--dir` | `string` | current working directory | Project root to scan |
54
+ | `--query` | `string` | `""` | Task or keywords for ranking (optional but recommended) |
55
+ | `--budget` | `number` | `32000` | Approximate token budget (char/4 estimator) |
56
+ | `--out` | `string` | none | Write markdown bundle to this path |
57
+ | `--min-keyword-score` | `number` | `0` when `--query` is set; disabled when query empty | Omit files with `keywordScore` at or below this value. Negative value disables the floor. |
58
+ | `--min-combined-score` | `number` | **`0.1`** when `--query` is set | Omit ranked files whose combined `score` is **strictly below** this value. **`-1`** disables. |
59
+ | `--keyword-recency-reference` | `number` | **`~0.055`** | Recency dampening reference (see above). **`0`** disables dampening. |
60
+
61
+ ### Ranking & bundle gate flags (reference)
62
+
63
+ These three flags only affect behavior when you pass a **non-empty `--query`** (except `--min-keyword-score`, which is ignored when the query is empty).
64
+
65
+ | Flag | Effect |
66
+ | --- | --- |
67
+ | **`--min-keyword-score`** | Drops files whose lexical **keyword score** (TF×IDF) is **≤** this value before spending token budget. **Default with query: `0`** (drops files with no query match). **`-1`** disables this floor. Raise it (e.g. `0.02`) if incidental token hits still get through. |
68
+ | **`--min-combined-score`** | Drops files whose **weighted rank score** (keyword + damped recency + type) is **&lt;** this value. **Default with query: `0.1`**. **`-1`** disables. Tune roughly **0.08–0.15** on many iOS repos; increase toward **0.2** if utility files still fill the tail. |
69
+ | **`--keyword-recency-reference`** | Controls **recency dampening**: recency is scaled by `min(1, keywordScore / reference)`. **Default: ~`0.055`**. **`0`** turns dampening off (legacy: full recency weight even for weak keyword hits). **Lower** the reference to dampen recency more aggressively for weak matches. |
70
+
71
+ ## Scanning defaults
72
+
73
+ **Extensions (whitelist):** `.swift`, `.m`, `.mm`, `.h`, `.plist`, `.kt`, `.kts`, `.java`, `.xml`, `.gradle`, `.properties`
74
+
75
+ **Always ignored (in addition to `.gitignore` / `.trimmerignore`):** e.g. `Pods/`, `DerivedData/`, `.gradle/`, `build/`, Core Data `*.xcmapping.xml`, common IDE and web artifacts. See `getDefaultMobileIgnorePatterns()` in source.
76
+
77
+ The CLI uses **metadata-first** scanning (`includeContent: false` during walk) and reads file contents only while ranking and bundling.
78
+
79
+ ## Example output
80
+
81
+ ```markdown
82
+ # Mobile Context Bundle
83
+
84
+ - Files included: 2
85
+ - Tokens used: 120
86
+ - Files skipped due to budget: 5
87
+ - Files skipped below keyword relevance floor: 6
88
+ - Files skipped below combined rank score: 3
89
+
90
+ ## File: `ios/App/AppDelegate.swift`
91
+
92
+ - Absolute path: `/path/to/MyApp/ios/App/AppDelegate.swift`
93
+ - Estimated tokens: 80
94
+ - Rank score: 0.812345
95
+ - Keyword score: 0.401000
96
+
97
+ ```
98
+ // file content
99
+ ```
100
+ ```
101
+
102
+ ## Library usage
103
+
104
+ ```ts
105
+ import {
106
+ scanMobileFiles,
107
+ rankMobileFiles,
108
+ createDefaultTokenizer,
109
+ buildBundle,
110
+ DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY,
111
+ formatBundleMarkdown,
112
+ } from "mobile-context-trimmer";
113
+
114
+ const rootDir = process.cwd();
115
+ const files = await scanMobileFiles({ rootDir, includeContent: false });
116
+ const ranked = await rankMobileFiles(files, {
117
+ query: "push notification",
118
+ rootDir,
119
+ });
120
+ const tokenizer = createDefaultTokenizer();
121
+ const bundle = await buildBundle(ranked, {
122
+ tokenBudget: 32000,
123
+ tokenizer,
124
+ minKeywordScore: 0,
125
+ minCombinedScore: DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY,
126
+ });
127
+ console.log(formatBundleMarkdown(bundle, rootDir));
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ npm install
134
+ npm test
135
+ npm run build
136
+ ```
137
+
138
+ ### CI
139
+
140
+ [GitHub Actions](.github/workflows/ci.yml) runs **`npm ci`**, **`npm test`**, and **`npm run build`** on **push** and **pull request** to `main` or `master` (Node 18, 20, 22).
141
+
142
+ ### Publishing to npm
143
+
144
+ - `package.json` includes **`publishConfig.access: "public"`** for scoped or first-time public packages.
145
+ - Set **`repository`**, **`bugs`**, and **`homepage`** in `package.json` to your GitHub URLs if they differ from the template.
146
+ - From a clean tree: `npm run build && npm test`, then `npm publish` (with npm login and version bump as needed).
147
+
148
+ Tests include unit coverage for scanner, tokenizer, bundler, ranker (TF‑IDF), Xcode path heuristics, optional git recency, and a CLI end-to-end run.
149
+
150
+ ## Contributing
151
+
152
+ Pull requests should stay focused, include tests for behavior changes, and pass `npm test && npm run build`.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,43 @@
1
+ import { type MobileScannedFile } from "./scanner.js";
2
+ import { type Tokenizer } from "./tokenizer.js";
3
+ /** Default `--min-combined-score` when `--query` is set (bundle stage). Use `-1` on the CLI to omit. */
4
+ export declare const DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY = 0.1;
5
+ export interface BundleOptions {
6
+ tokenBudget: number;
7
+ tokenizer: Tokenizer;
8
+ /**
9
+ * When set, files from ranked output with `keywordScore <= minKeywordScore` are omitted
10
+ * so high-recency boilerplate cannot fill the bundle. Ignored for inputs without `keywordScore`.
11
+ * Use `0` to require strictly positive lexical relevance vs the query.
12
+ */
13
+ minKeywordScore?: number;
14
+ /**
15
+ * When set, ranked files with combined `score` (keyword + recency + type) **strictly below**
16
+ * this value are omitted so weakly relevant but recently touched utilities do not fill the tail.
17
+ */
18
+ minCombinedScore?: number;
19
+ }
20
+ export interface BundleItem {
21
+ path: string;
22
+ content: string;
23
+ estimatedTokens: number;
24
+ /** Present when the bundled file came from ranked output. */
25
+ score?: number;
26
+ /** Present when the bundled file came from ranked output. */
27
+ keywordScore?: number;
28
+ }
29
+ export interface BundleResult {
30
+ items: BundleItem[];
31
+ usedTokens: number;
32
+ skippedFully: number;
33
+ skippedBelowRelevance: number;
34
+ skippedBelowCombinedScore: number;
35
+ }
36
+ /**
37
+ * Builds a markdown-ready bundle within a token budget.
38
+ */
39
+ export declare function buildBundle(files: MobileScannedFile[], options: BundleOptions): Promise<BundleResult>;
40
+ /**
41
+ * Formats bundle output as markdown.
42
+ */
43
+ export declare function formatBundleMarkdown(bundle: BundleResult, rootDir: string): string;
@@ -0,0 +1,99 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ /** Default `--min-combined-score` when `--query` is set (bundle stage). Use `-1` on the CLI to omit. */
4
+ export const DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY = 0.1;
5
+ /**
6
+ * Builds a markdown-ready bundle within a token budget.
7
+ */
8
+ export async function buildBundle(files, options) {
9
+ const items = [];
10
+ let usedTokens = 0;
11
+ let skippedFully = 0;
12
+ let skippedBelowRelevance = 0;
13
+ let skippedBelowCombinedScore = 0;
14
+ const floor = options.minKeywordScore;
15
+ const minCombined = options.minCombinedScore;
16
+ for (const file of files) {
17
+ if (floor !== undefined && hasKeywordScore(file)) {
18
+ if (file.keywordScore <= floor) {
19
+ skippedBelowRelevance += 1;
20
+ continue;
21
+ }
22
+ }
23
+ if (minCombined !== undefined && hasRankScore(file)) {
24
+ if (file.score < minCombined) {
25
+ skippedBelowCombinedScore += 1;
26
+ continue;
27
+ }
28
+ }
29
+ const content = file.content ?? (await fs.readFile(file.absolutePath, "utf8"));
30
+ if (content.trim().length === 0) {
31
+ continue;
32
+ }
33
+ const estimatedTokens = options.tokenizer.estimateTokens(content);
34
+ if (estimatedTokens === 0) {
35
+ continue;
36
+ }
37
+ if (usedTokens + estimatedTokens > options.tokenBudget) {
38
+ skippedFully += 1;
39
+ continue;
40
+ }
41
+ const item = {
42
+ path: file.relativePath,
43
+ content,
44
+ estimatedTokens
45
+ };
46
+ if (hasRankScore(file)) {
47
+ item.score = file.score;
48
+ }
49
+ if (hasKeywordScore(file)) {
50
+ item.keywordScore = file.keywordScore;
51
+ }
52
+ items.push(item);
53
+ usedTokens += estimatedTokens;
54
+ }
55
+ return { items, usedTokens, skippedFully, skippedBelowRelevance, skippedBelowCombinedScore };
56
+ }
57
+ function hasKeywordScore(file) {
58
+ return ("keywordScore" in file &&
59
+ typeof file.keywordScore === "number" &&
60
+ !Number.isNaN(file.keywordScore));
61
+ }
62
+ function hasRankScore(file) {
63
+ return ("score" in file &&
64
+ typeof file.score === "number" &&
65
+ !Number.isNaN(file.score));
66
+ }
67
+ /**
68
+ * Formats bundle output as markdown.
69
+ */
70
+ export function formatBundleMarkdown(bundle, rootDir) {
71
+ const sections = bundle.items.map((item) => {
72
+ const lines = [
73
+ `## File: \`${item.path}\``,
74
+ "",
75
+ `- Absolute path: \`${path.resolve(rootDir, item.path)}\``,
76
+ `- Estimated tokens: ${item.estimatedTokens}`
77
+ ];
78
+ if (item.score !== undefined) {
79
+ lines.push(`- Rank score: ${item.score.toFixed(6)}`);
80
+ }
81
+ if (item.keywordScore !== undefined) {
82
+ lines.push(`- Keyword score: ${item.keywordScore.toFixed(6)}`);
83
+ }
84
+ lines.push("", "```", item.content, "```");
85
+ return lines.join("\n");
86
+ });
87
+ return [
88
+ "# Mobile Context Bundle",
89
+ "",
90
+ `- Files included: ${bundle.items.length}`,
91
+ `- Tokens used: ${bundle.usedTokens}`,
92
+ `- Files skipped due to budget: ${bundle.skippedFully}`,
93
+ `- Files skipped below keyword relevance floor: ${bundle.skippedBelowRelevance}`,
94
+ `- Files skipped below combined rank score: ${bundle.skippedBelowCombinedScore}`,
95
+ "",
96
+ ...sections
97
+ ].join("\n");
98
+ }
99
+ //# sourceMappingURL=bundler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundler.js","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import yargs from "yargs";
5
+ import { hideBin } from "yargs/helpers";
6
+ import { buildBundle, DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY, formatBundleMarkdown } from "./bundler.js";
7
+ import { rankMobileFiles } from "./ranker.js";
8
+ import { scanMobileFiles } from "./scanner.js";
9
+ import { createDefaultTokenizer } from "./tokenizer.js";
10
+ void yargs(hideBin(process.argv))
11
+ .scriptName("mobile-context-trimmer")
12
+ .usage("$0 --dir ./repo --budget 32000 --out mobile-context.md")
13
+ .option("dir", {
14
+ type: "string",
15
+ default: process.cwd(),
16
+ describe: "Root directory of iOS/Android project"
17
+ })
18
+ .option("budget", {
19
+ type: "number",
20
+ default: 32000,
21
+ describe: "Token budget for selected files"
22
+ })
23
+ .option("out", {
24
+ type: "string",
25
+ describe: "Optional output markdown file path"
26
+ })
27
+ .option("query", {
28
+ type: "string",
29
+ default: "",
30
+ describe: "Optional task/query to rank files before bundling"
31
+ })
32
+ .option("min-keyword-score", {
33
+ type: "number",
34
+ describe: "Keyword relevance floor when --query is set (default: 0, omit files with no query match). Use a negative value to disable."
35
+ })
36
+ .option("min-combined-score", {
37
+ type: "number",
38
+ describe: `Minimum weighted rank score to include a file (default ${DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY} when --query is set). Use -1 to disable. Calibrate ~0.08–0.15 for many iOS repos.`
39
+ })
40
+ .option("keyword-recency-reference", {
41
+ type: "number",
42
+ describe: "Keyword TF-IDF reference for recency dampening when --query is set (default ~0.055). Lower = stricter. Use 0 to disable dampening."
43
+ })
44
+ .command("$0", "Build a mobile context bundle", () => { }, async (argv) => {
45
+ const rootDir = path.resolve(String(argv.dir));
46
+ const budget = Math.max(1, Math.floor(Number(argv.budget)));
47
+ const queryTrim = String(argv.query ?? "").trim();
48
+ const rawMin = argv["min-keyword-score"];
49
+ let minKeywordScore;
50
+ if (queryTrim) {
51
+ if (rawMin !== undefined && rawMin < 0) {
52
+ minKeywordScore = undefined;
53
+ }
54
+ else {
55
+ minKeywordScore = rawMin !== undefined ? rawMin : 0;
56
+ }
57
+ }
58
+ const files = await scanMobileFiles({ rootDir, includeContent: false });
59
+ const kwRef = argv["keyword-recency-reference"];
60
+ const rankedFiles = await rankMobileFiles(files, {
61
+ query: String(argv.query ?? ""),
62
+ rootDir,
63
+ ...(kwRef !== undefined && !Number.isNaN(kwRef) ? { keywordRecencyReference: kwRef } : {})
64
+ });
65
+ const rawCombined = argv["min-combined-score"];
66
+ let minCombinedScore;
67
+ if (queryTrim) {
68
+ if (rawCombined !== undefined && !Number.isNaN(rawCombined)) {
69
+ minCombinedScore = rawCombined < 0 ? undefined : rawCombined;
70
+ }
71
+ else {
72
+ minCombinedScore = DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY;
73
+ }
74
+ }
75
+ const bundle = await buildBundle(rankedFiles, {
76
+ tokenBudget: budget,
77
+ tokenizer: createDefaultTokenizer(),
78
+ minKeywordScore,
79
+ minCombinedScore
80
+ });
81
+ const output = formatBundleMarkdown(bundle, rootDir);
82
+ if (argv.out) {
83
+ const outPath = path.resolve(String(argv.out));
84
+ await fs.writeFile(outPath, output, "utf8");
85
+ process.stderr.write(`mobile-context-trimmer: wrote ${bundle.items.length} files (${bundle.usedTokens} tokens) to ${outPath}\n`);
86
+ return;
87
+ }
88
+ process.stdout.write(`${output}\n`);
89
+ })
90
+ .help()
91
+ .strict()
92
+ .parseAsync();
93
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns the package name for quick sanity checks.
3
+ */
4
+ export declare function getLibraryName(): string;
5
+ /**
6
+ * Returns default file extensions for native iOS/Android scanning.
7
+ */
8
+ export declare function getDefaultMobileExtensions(): string[];
9
+ /**
10
+ * Returns default ignore patterns for native mobile repositories.
11
+ */
12
+ export declare function getDefaultMobileIgnorePatterns(): string[];
13
+ export type MobilePlatform = "ios" | "android" | "mixed" | "unknown";
14
+ /**
15
+ * Detects mobile platform focus from a list of file paths.
16
+ */
17
+ export declare function detectMobilePlatform(paths: string[]): MobilePlatform;
18
+ export { scanMobileFiles, type MobileScannedFile, type MobileScanOptions } from "./scanner.js";
19
+ export { createDefaultTokenizer, estimateChar4Tokens, type Tokenizer } from "./tokenizer.js";
20
+ export { buildBundle, DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY, formatBundleMarkdown, type BundleItem, type BundleOptions, type BundleResult } from "./bundler.js";
21
+ export { DEFAULT_KEYWORD_RECENCY_REFERENCE, rankMobileFiles, tokenizeQuery, computeKeywordScoreFromStats, isGitRecencyUnreliablePath, type RankMobileOptions, type RankedMobileFile } from "./ranker.js";
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Returns the package name for quick sanity checks.
3
+ */
4
+ export function getLibraryName() {
5
+ return "mobile-context-trimmer";
6
+ }
7
+ /**
8
+ * Returns default file extensions for native iOS/Android scanning.
9
+ */
10
+ export function getDefaultMobileExtensions() {
11
+ return [
12
+ ".swift",
13
+ ".m",
14
+ ".mm",
15
+ ".h",
16
+ ".plist",
17
+ ".kt",
18
+ ".kts",
19
+ ".java",
20
+ ".xml",
21
+ ".gradle",
22
+ ".properties"
23
+ ];
24
+ }
25
+ /**
26
+ * Returns default ignore patterns for native mobile repositories.
27
+ */
28
+ export function getDefaultMobileIgnorePatterns() {
29
+ return [
30
+ ".git/",
31
+ "node_modules/",
32
+ "Pods/",
33
+ "DerivedData/",
34
+ ".gradle/",
35
+ ".idea/",
36
+ "build/",
37
+ "**/build/",
38
+ ".cxx/",
39
+ ".dart_tool/",
40
+ ".next/",
41
+ ".turbo/",
42
+ "*.xcworkspace/xcuserdata/",
43
+ "*.xcodeproj/xcuserdata/",
44
+ "*.xcmapping.xml"
45
+ ];
46
+ }
47
+ /**
48
+ * Detects mobile platform focus from a list of file paths.
49
+ */
50
+ export function detectMobilePlatform(paths) {
51
+ let hasIos = false;
52
+ let hasAndroid = false;
53
+ for (const rawPath of paths) {
54
+ const normalized = rawPath.toLowerCase();
55
+ if (normalized.endsWith(".swift") ||
56
+ normalized.endsWith(".m") ||
57
+ normalized.endsWith(".mm") ||
58
+ normalized.endsWith(".plist") ||
59
+ normalized.includes("/ios/") ||
60
+ normalized.includes(".xcodeproj")) {
61
+ hasIos = true;
62
+ }
63
+ if (normalized.endsWith(".kt") ||
64
+ normalized.endsWith(".kts") ||
65
+ normalized.endsWith(".java") ||
66
+ normalized.endsWith("androidmanifest.xml") ||
67
+ normalized.includes("/android/") ||
68
+ normalized.includes("build.gradle")) {
69
+ hasAndroid = true;
70
+ }
71
+ }
72
+ if (hasIos && hasAndroid) {
73
+ return "mixed";
74
+ }
75
+ if (hasIos) {
76
+ return "ios";
77
+ }
78
+ if (hasAndroid) {
79
+ return "android";
80
+ }
81
+ return "unknown";
82
+ }
83
+ export { scanMobileFiles } from "./scanner.js";
84
+ export { createDefaultTokenizer, estimateChar4Tokens } from "./tokenizer.js";
85
+ export { buildBundle, DEFAULT_MIN_COMBINED_SCORE_WITH_QUERY, formatBundleMarkdown } from "./bundler.js";
86
+ export { DEFAULT_KEYWORD_RECENCY_REFERENCE, rankMobileFiles, tokenizeQuery, computeKeywordScoreFromStats, isGitRecencyUnreliablePath } from "./ranker.js";
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO;QACL,QAAQ;QACR,IAAI;QACJ,KAAK;QACL,IAAI;QACJ,QAAQ;QACR,KAAK;QACL,MAAM;QACN,OAAO;QACP,MAAM;QACN,SAAS;QACT,aAAa;KACd,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,8BAA8B;IAC5C,OAAO;QACL,OAAO;QACP,eAAe;QACf,OAAO;QACP,cAAc;QACd,UAAU;QACV,QAAQ;QACR,QAAQ;QACR,WAAW;QACX,OAAO;QACP,aAAa;QACb,QAAQ;QACR,SAAS;QACT,2BAA2B;QAC3B,yBAAyB;QACzB,iBAAiB;KAClB,CAAC;AACJ,CAAC;AAID;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAe;IAClD,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QACzC,IACE,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC7B,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;YACzB,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC1B,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC7B,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC;YAC5B,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,EACjC,CAAC;YACD,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,IACE,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC1B,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC3B,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC;YAC5B,UAAU,CAAC,QAAQ,CAAC,qBAAqB,CAAC;YAC1C,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC;YAChC,UAAU,CAAC,QAAQ,CAAC,cAAc,CAAC,EACnC,CAAC;YACD,UAAU,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED,IAAI,MAAM,IAAI,UAAU,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,OAAO,EAAE,eAAe,EAAkD,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAC7F,OAAO,EACL,WAAW,EACX,qCAAqC,EACrC,oBAAoB,EAIrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iCAAiC,EACjC,eAAe,EACf,aAAa,EACb,4BAA4B,EAC5B,0BAA0B,EAG3B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { type MobileScannedFile } from "./scanner.js";
2
+ /**
3
+ * Keyword TF‑IDF score at or above this value earns full recency weight; weaker matches scale recency down linearly.
4
+ * Tuned so incidental hits (e.g. `subscriptions` as a property name) do not inherit a full 30% recency boost.
5
+ */
6
+ export declare const DEFAULT_KEYWORD_RECENCY_REFERENCE = 0.055;
7
+ export interface RankMobileOptions {
8
+ query: string;
9
+ /** Repository root; required for git recency and path resolution. */
10
+ rootDir: string;
11
+ keywordWeight?: number;
12
+ recencyWeight?: number;
13
+ typeWeight?: number;
14
+ typePriority?: Record<string, number>;
15
+ /**
16
+ * When the query has tokens, recency is multiplied by `min(1, keywordScore / reference)` unless disabled.
17
+ * Set to `0` to disable dampening (legacy full recency). Default: {@link DEFAULT_KEYWORD_RECENCY_REFERENCE}.
18
+ */
19
+ keywordRecencyReference?: number;
20
+ }
21
+ export interface RankedMobileFile extends MobileScannedFile {
22
+ score: number;
23
+ keywordScore: number;
24
+ recencyScore: number;
25
+ typeScore: number;
26
+ lastModifiedEpochMs: number;
27
+ }
28
+ interface FileKeywordStats {
29
+ relativePath: string;
30
+ matchesByToken: Map<string, number>;
31
+ contentTokenCount: number;
32
+ }
33
+ /**
34
+ * Paths where git timestamps are dominated by Xcode merges/rewrites; use filesystem mtime instead.
35
+ */
36
+ export declare function isGitRecencyUnreliablePath(relativePath: string): boolean;
37
+ /**
38
+ * Ranks mobile files using TF-IDF-style query match, Xcode-safe recency, and extension priority.
39
+ */
40
+ export declare function rankMobileFiles(files: MobileScannedFile[], options: RankMobileOptions): Promise<RankedMobileFile[]>;
41
+ /**
42
+ * Tokenizes a free text query into searchable terms.
43
+ */
44
+ export declare function tokenizeQuery(query: string): string[];
45
+ /**
46
+ * Computes TF-IDF style score using precomputed per-file stats and corpus IDF.
47
+ */
48
+ export declare function computeKeywordScoreFromStats(stats: FileKeywordStats | undefined, queryTokens: string[], inverseDocumentFrequency: Map<string, number>): number;
49
+ export {};
package/dist/ranker.js ADDED
@@ -0,0 +1,234 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { promisify } from "node:util";
3
+ import { execFile } from "node:child_process";
4
+ import path from "node:path";
5
+ const execFileAsync = promisify(execFile);
6
+ /**
7
+ * Keyword TF‑IDF score at or above this value earns full recency weight; weaker matches scale recency down linearly.
8
+ * Tuned so incidental hits (e.g. `subscriptions` as a property name) do not inherit a full 30% recency boost.
9
+ */
10
+ export const DEFAULT_KEYWORD_RECENCY_REFERENCE = 0.055;
11
+ const DEFAULT_TYPE_PRIORITY = {
12
+ ".swift": 1,
13
+ ".kt": 1,
14
+ ".kts": 0.95,
15
+ ".java": 0.9,
16
+ ".xml": 0.8,
17
+ ".plist": 0.7,
18
+ ".gradle": 0.75,
19
+ ".properties": 0.5
20
+ };
21
+ /**
22
+ * Paths where git timestamps are dominated by Xcode merges/rewrites; use filesystem mtime instead.
23
+ */
24
+ export function isGitRecencyUnreliablePath(relativePath) {
25
+ const normalized = relativePath.split(path.sep).join("/").toLowerCase();
26
+ if (normalized.includes(".xcodeproj/")) {
27
+ return true;
28
+ }
29
+ if (normalized.endsWith(".pbxproj")) {
30
+ return true;
31
+ }
32
+ if (normalized.includes(".xcuserdata/")) {
33
+ return true;
34
+ }
35
+ if (normalized.includes(".xcworkspace/xcuserdata/")) {
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ /**
41
+ * Ranks mobile files using TF-IDF-style query match, Xcode-safe recency, and extension priority.
42
+ */
43
+ export async function rankMobileFiles(files, options) {
44
+ const rootDir = path.resolve(options.rootDir);
45
+ const tokens = tokenizeQuery(options.query);
46
+ const keywordWeight = options.keywordWeight ?? 0.55;
47
+ const recencyWeight = options.recencyWeight ?? 0.3;
48
+ const typeWeight = options.typeWeight ?? 0.15;
49
+ const priority = {
50
+ ...DEFAULT_TYPE_PRIORITY,
51
+ ...normalizeTypePriority(options.typePriority ?? {})
52
+ };
53
+ const recencyRef = options.keywordRecencyReference === 0
54
+ ? null
55
+ : (options.keywordRecencyReference ?? DEFAULT_KEYWORD_RECENCY_REFERENCE);
56
+ const keywordStats = await buildKeywordStats(files, tokens);
57
+ const idf = buildInverseDocumentFrequency(keywordStats, tokens);
58
+ const timestamps = await readMobileModifiedTimestamps(rootDir, files);
59
+ const recencyRange = buildRange(Array.from(timestamps.values()));
60
+ const ranked = [];
61
+ for (const file of files) {
62
+ const stats = keywordStats.get(file.relativePath);
63
+ const keywordScore = computeKeywordScoreFromStats(stats, tokens, idf);
64
+ const lastModifiedEpochMs = timestamps.get(file.relativePath) ?? 0;
65
+ const recencyScore = normalizeFromRange(lastModifiedEpochMs, recencyRange.min, recencyRange.max);
66
+ const typeScore = priority[file.extension] ?? 0;
67
+ const recencyMultiplier = tokens.length === 0 || recencyRef === null ? 1 : Math.min(1, keywordScore / recencyRef);
68
+ const score = keywordScore * keywordWeight +
69
+ recencyScore * recencyWeight * recencyMultiplier +
70
+ typeScore * typeWeight;
71
+ const content = file.content ?? (await fs.readFile(file.absolutePath, "utf8"));
72
+ ranked.push({
73
+ ...file,
74
+ score,
75
+ keywordScore,
76
+ recencyScore,
77
+ typeScore,
78
+ lastModifiedEpochMs,
79
+ content
80
+ });
81
+ }
82
+ return ranked.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
83
+ }
84
+ /**
85
+ * Tokenizes a free text query into searchable terms.
86
+ */
87
+ export function tokenizeQuery(query) {
88
+ return query
89
+ .toLowerCase()
90
+ .split(/[^a-z0-9_]+/)
91
+ .map((part) => part.trim())
92
+ .filter((part) => part.length > 1);
93
+ }
94
+ /**
95
+ * Computes TF-IDF style score using precomputed per-file stats and corpus IDF.
96
+ */
97
+ export function computeKeywordScoreFromStats(stats, queryTokens, inverseDocumentFrequency) {
98
+ if (!stats || queryTokens.length === 0) {
99
+ return 0;
100
+ }
101
+ let total = 0;
102
+ for (const token of queryTokens) {
103
+ const matches = stats.matchesByToken.get(token) ?? 0;
104
+ const tf = matches / Math.max(1, stats.contentTokenCount);
105
+ total += tf * (inverseDocumentFrequency.get(token) ?? 0);
106
+ }
107
+ return total;
108
+ }
109
+ function countTokenOccurrences(text, token) {
110
+ if (!token) {
111
+ return 0;
112
+ }
113
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
+ const regex = new RegExp(`\\b${escaped}\\b`, "gi");
115
+ return text.match(regex)?.length ?? 0;
116
+ }
117
+ function buildInverseDocumentFrequency(statsByPath, queryTokens) {
118
+ const totalDocs = Math.max(1, statsByPath.size);
119
+ const result = new Map();
120
+ for (const token of queryTokens) {
121
+ let docsContainingToken = 0;
122
+ for (const stats of statsByPath.values()) {
123
+ if ((stats.matchesByToken.get(token) ?? 0) > 0) {
124
+ docsContainingToken += 1;
125
+ }
126
+ }
127
+ const idf = Math.log((1 + totalDocs) / (1 + docsContainingToken)) + 1;
128
+ result.set(token, idf);
129
+ }
130
+ return result;
131
+ }
132
+ async function buildKeywordStats(files, tokens) {
133
+ const result = new Map();
134
+ await Promise.all(files.map(async (file) => {
135
+ const content = file.content ?? (await fs.readFile(file.absolutePath, "utf8"));
136
+ const lowered = content.toLowerCase();
137
+ const contentTokenCount = Math.max(1, lowered.split(/\s+/).length);
138
+ const matchesByToken = new Map();
139
+ for (const token of tokens) {
140
+ matchesByToken.set(token, countTokenOccurrences(lowered, token));
141
+ }
142
+ result.set(file.relativePath, {
143
+ relativePath: file.relativePath,
144
+ matchesByToken,
145
+ contentTokenCount
146
+ });
147
+ }));
148
+ return result;
149
+ }
150
+ async function readMobileModifiedTimestamps(rootDir, files) {
151
+ const output = new Map();
152
+ const gitTimestamps = await readGitTimestampsBatch(rootDir);
153
+ await Promise.all(files.map(async (file) => {
154
+ const fullPath = path.resolve(rootDir, file.relativePath);
155
+ if (isGitRecencyUnreliablePath(file.relativePath)) {
156
+ try {
157
+ const stat = await fs.stat(fullPath);
158
+ output.set(file.relativePath, stat.mtimeMs);
159
+ }
160
+ catch {
161
+ output.set(file.relativePath, 0);
162
+ }
163
+ return;
164
+ }
165
+ const gitTs = gitTimestamps.get(file.relativePath) ?? 0;
166
+ if (gitTs > 0) {
167
+ output.set(file.relativePath, gitTs);
168
+ return;
169
+ }
170
+ try {
171
+ const stat = await fs.stat(fullPath);
172
+ output.set(file.relativePath, stat.mtimeMs);
173
+ }
174
+ catch {
175
+ output.set(file.relativePath, 0);
176
+ }
177
+ }));
178
+ return output;
179
+ }
180
+ async function readGitTimestampsBatch(rootDir) {
181
+ const output = new Map();
182
+ try {
183
+ const { stdout } = await execFileAsync("git", ["log", "--name-only", "--format=%ct"], {
184
+ cwd: rootDir
185
+ });
186
+ const lines = stdout.split(/\r?\n/);
187
+ let currentTimestamp = 0;
188
+ for (const rawLine of lines) {
189
+ const line = rawLine.trim();
190
+ if (!line) {
191
+ continue;
192
+ }
193
+ if (/^\d+$/.test(line)) {
194
+ currentTimestamp = Number.parseInt(line, 10) * 1000;
195
+ continue;
196
+ }
197
+ if (currentTimestamp <= 0) {
198
+ continue;
199
+ }
200
+ const normalizedPath = line.split(path.sep).join("/");
201
+ if (isGitRecencyUnreliablePath(normalizedPath)) {
202
+ continue;
203
+ }
204
+ if (!output.has(normalizedPath)) {
205
+ output.set(normalizedPath, currentTimestamp);
206
+ }
207
+ }
208
+ }
209
+ catch {
210
+ return output;
211
+ }
212
+ return output;
213
+ }
214
+ function buildRange(values) {
215
+ if (values.length === 0) {
216
+ return { min: 0, max: 0 };
217
+ }
218
+ return { min: Math.min(...values), max: Math.max(...values) };
219
+ }
220
+ function normalizeFromRange(value, min, max) {
221
+ if (max <= min) {
222
+ return 0;
223
+ }
224
+ return (value - min) / (max - min);
225
+ }
226
+ function normalizeTypePriority(priority) {
227
+ const normalized = {};
228
+ for (const [ext, score] of Object.entries(priority)) {
229
+ const key = ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`;
230
+ normalized[key] = score;
231
+ }
232
+ return normalized;
233
+ }
234
+ //# sourceMappingURL=ranker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranker.js","sourceRoot":"","sources":["../src/ranker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export interface MobileScanOptions {
2
+ rootDir: string;
3
+ extensions?: string[];
4
+ includeContent?: boolean;
5
+ includeHidden?: boolean;
6
+ }
7
+ export interface MobileScannedFile {
8
+ absolutePath: string;
9
+ relativePath: string;
10
+ extension: string;
11
+ sizeBytes: number;
12
+ content?: string;
13
+ }
14
+ /**
15
+ * Scans a mobile repository with iOS/Android-aware defaults.
16
+ */
17
+ export declare function scanMobileFiles(options: MobileScanOptions): Promise<MobileScannedFile[]>;
18
+ /**
19
+ * Normalizes extension strings with leading dots and lowercase.
20
+ */
21
+ export declare function normalizeExtensions(extensions: string[]): Set<string>;
@@ -0,0 +1,79 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import ignore from "ignore";
4
+ import { getDefaultMobileExtensions, getDefaultMobileIgnorePatterns } from "./index.js";
5
+ /**
6
+ * Scans a mobile repository with iOS/Android-aware defaults.
7
+ */
8
+ export async function scanMobileFiles(options) {
9
+ const rootDir = path.resolve(options.rootDir);
10
+ const includeContent = options.includeContent ?? false;
11
+ const includeHidden = options.includeHidden ?? false;
12
+ const extensions = normalizeExtensions(options.extensions ?? getDefaultMobileExtensions());
13
+ const ig = await createIgnoreMatcher(rootDir, includeHidden);
14
+ const files = [];
15
+ await walk(rootDir, rootDir, ig, extensions, includeContent, files);
16
+ return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
17
+ }
18
+ /**
19
+ * Normalizes extension strings with leading dots and lowercase.
20
+ */
21
+ export function normalizeExtensions(extensions) {
22
+ return new Set(extensions.map((ext) => (ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`)));
23
+ }
24
+ async function walk(rootDir, currentDir, ig, extensions, includeContent, out) {
25
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const absolutePath = path.join(currentDir, entry.name);
28
+ const relativePath = normalizeRelativePath(path.relative(rootDir, absolutePath));
29
+ if (!relativePath || ig.ignores(relativePath)) {
30
+ continue;
31
+ }
32
+ if (entry.isDirectory()) {
33
+ await walk(rootDir, absolutePath, ig, extensions, includeContent, out);
34
+ continue;
35
+ }
36
+ if (!entry.isFile()) {
37
+ continue;
38
+ }
39
+ const extension = path.extname(entry.name).toLowerCase();
40
+ if (!extensions.has(extension)) {
41
+ continue;
42
+ }
43
+ const stat = await fs.stat(absolutePath);
44
+ const content = includeContent ? await fs.readFile(absolutePath, "utf8") : undefined;
45
+ out.push({
46
+ absolutePath,
47
+ relativePath,
48
+ extension,
49
+ sizeBytes: stat.size,
50
+ content
51
+ });
52
+ }
53
+ }
54
+ async function createIgnoreMatcher(rootDir, includeHidden) {
55
+ const ig = ignore();
56
+ ig.add(getDefaultMobileIgnorePatterns());
57
+ ig.add(await readIgnoreFile(path.join(rootDir, ".gitignore")));
58
+ ig.add(await readIgnoreFile(path.join(rootDir, ".trimmerignore")));
59
+ if (!includeHidden) {
60
+ ig.add(".*");
61
+ }
62
+ return ig;
63
+ }
64
+ async function readIgnoreFile(filePath) {
65
+ try {
66
+ const content = await fs.readFile(filePath, "utf8");
67
+ return content
68
+ .split(/\r?\n/)
69
+ .map((line) => line.trim())
70
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }
76
+ function normalizeRelativePath(value) {
77
+ return value.split(path.sep).join("/");
78
+ }
79
+ //# sourceMappingURL=scanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface Tokenizer {
2
+ estimateTokens(text: string): number;
3
+ }
4
+ /**
5
+ * Char-based token estimation (chars/4) for lightweight defaults.
6
+ */
7
+ export declare function estimateChar4Tokens(text: string): number;
8
+ /**
9
+ * Creates the default tokenizer implementation.
10
+ */
11
+ export declare function createDefaultTokenizer(): Tokenizer;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Char-based token estimation (chars/4) for lightweight defaults.
3
+ */
4
+ export function estimateChar4Tokens(text) {
5
+ if (!text) {
6
+ return 0;
7
+ }
8
+ return Math.ceil(text.length / 4);
9
+ }
10
+ /**
11
+ * Creates the default tokenizer implementation.
12
+ */
13
+ export function createDefaultTokenizer() {
14
+ return {
15
+ estimateTokens(text) {
16
+ return estimateChar4Tokens(text);
17
+ }
18
+ };
19
+ }
20
+ //# sourceMappingURL=tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,cAAc,CAAC,IAAY;YACzB,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "mobile-context-trimmer",
3
+ "version": "0.1.0",
4
+ "description": "CLI and library for building LLM context bundles for native iOS and Android projects",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "bin": {
12
+ "mobile-context-trimmer": "dist/cli.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=20.19.0"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "dev": "tsx src/cli.ts",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "prepublishOnly": "npm test && npm run build"
23
+ },
24
+ "keywords": [
25
+ "ios",
26
+ "android",
27
+ "mobile",
28
+ "swift",
29
+ "kotlin",
30
+ "llm",
31
+ "context",
32
+ "cli",
33
+ "typescript",
34
+ "codebase",
35
+ "xcode",
36
+ "gradle"
37
+ ],
38
+ "author": "isonka",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/isonka/mobile-context-trimmer.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/isonka/mobile-context-trimmer/issues"
46
+ },
47
+ "homepage": "https://github.com/isonka/mobile-context-trimmer#readme",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "type": "module",
52
+ "dependencies": {
53
+ "ignore": "^7.0.5",
54
+ "yargs": "^18.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@emnapi/core": "1.9.2",
58
+ "@emnapi/runtime": "1.9.2",
59
+ "@types/node": "^25.5.2",
60
+ "@types/yargs": "^17.0.35",
61
+ "tsx": "^4.21.0",
62
+ "typescript": "^6.0.2",
63
+ "vitest": "^4.1.3"
64
+ }
65
+ }