ctxshot 0.1.0 → 0.2.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/README.md +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -13
- package/dist/core/format.d.ts +12 -0
- package/dist/core/format.js +75 -0
- package/dist/core/git.d.ts +8 -0
- package/dist/core/git.js +35 -0
- package/dist/core/ignore.d.ts +4 -0
- package/dist/core/ignore.js +62 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/pack.d.ts +22 -0
- package/dist/core/pack.js +35 -0
- package/dist/core/scan.d.ts +14 -0
- package/dist/core/scan.js +122 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -14,6 +14,25 @@ npx ctxshot --compact -o .ai/context.md
|
|
|
14
14
|
npx ctxshot --diff
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
### MCP(Claude Code / Cursor)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx ctxshot-mcp # stdio MCP server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"ctxshot": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "ctxshot-mcp@latest"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Tools: `session_brief`(每日写 `.ai/context.md`)· `pack_context` · `context_stats`
|
|
35
|
+
|
|
17
36
|
每次新开 Claude Code / Cursor 会话都要重新解释项目?复制整份 README 又费 token。`ctxshot` 把「项目全貌」压成一份 **AI-ready brief**,成为你每天的固定起手式。
|
|
18
37
|
|
|
19
38
|
---
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,8 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { resolve, dirname } from "node:path";
|
|
5
5
|
import pc from "picocolors";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import { formatBrief } from "./format.js";
|
|
9
|
-
const VERSION = "0.1.0";
|
|
6
|
+
import { packContext } from "./core/pack.js";
|
|
7
|
+
const VERSION = "0.2.0";
|
|
10
8
|
function help() {
|
|
11
9
|
console.log(`
|
|
12
10
|
${pc.bold("ctxshot")} — 开 AI 编程会话前,3 秒打包项目上下文(结构 / 脚本 / 最近改动)。
|
|
@@ -29,7 +27,7 @@ ${pc.bold("ctxshot")} — 开 AI 编程会话前,3 秒打包项目上下文(
|
|
|
29
27
|
npx ctxshot --compact -o .ai/context.md
|
|
30
28
|
npx ctxshot --diff
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
MCP: npx ctxshot-mcp — pack_context / session_brief tools for Claude Code & Cursor
|
|
33
31
|
`);
|
|
34
32
|
}
|
|
35
33
|
async function main() {
|
|
@@ -55,19 +53,21 @@ async function main() {
|
|
|
55
53
|
}
|
|
56
54
|
const root = resolve(process.cwd());
|
|
57
55
|
const compact = values.compact ?? false;
|
|
58
|
-
const depth = values.depth ? parseInt(values.depth, 10) :
|
|
59
|
-
const maxEntries = values.max ? parseInt(values.max, 10) :
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
const depth = values.depth ? parseInt(values.depth, 10) : undefined;
|
|
57
|
+
const maxEntries = values.max ? parseInt(values.max, 10) : undefined;
|
|
58
|
+
const { markdown: text, stats } = packContext({
|
|
59
|
+
root,
|
|
60
|
+
compact,
|
|
61
|
+
diff: values.diff ?? false,
|
|
62
|
+
depth,
|
|
63
|
+
maxEntries,
|
|
64
|
+
});
|
|
65
65
|
if (values.out) {
|
|
66
66
|
const outPath = resolve(root, values.out);
|
|
67
67
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
68
68
|
writeFileSync(outPath, text, "utf8");
|
|
69
69
|
console.error(pc.green(`已写入 ${values.out}`));
|
|
70
|
-
console.error(pc.dim(`${
|
|
70
|
+
console.error(pc.dim(`${stats.lineCount} 行,约 ${stats.estimatedTokens} tokens(粗估)`));
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
73
|
console.log(text);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ManifestSummary } from "./scan.js";
|
|
2
|
+
import type { GitSummary } from "./git.js";
|
|
3
|
+
export interface FormatOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
tree: string[];
|
|
6
|
+
manifests: ManifestSummary[];
|
|
7
|
+
snippets: string[];
|
|
8
|
+
git: GitSummary | null;
|
|
9
|
+
compact: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function estimateTokens(text: string): number;
|
|
12
|
+
export declare function formatBrief(opts: FormatOptions): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function estimateTokens(text) {
|
|
2
|
+
return Math.round(text.length / 4);
|
|
3
|
+
}
|
|
4
|
+
export function formatBrief(opts) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push("# Project context (ctxshot)");
|
|
7
|
+
lines.push("");
|
|
8
|
+
lines.push(`Root: \`${opts.root}\``);
|
|
9
|
+
const bodyPreview = [
|
|
10
|
+
...opts.tree,
|
|
11
|
+
...opts.snippets,
|
|
12
|
+
JSON.stringify(opts.manifests),
|
|
13
|
+
].join("\n");
|
|
14
|
+
const estTokens = estimateTokens(bodyPreview);
|
|
15
|
+
lines.push(`Mode: ${opts.compact ? "compact · daily session brief" : "full"} · ~${estTokens} tokens (estimate)`);
|
|
16
|
+
lines.push(`Note: lightweight overview only — for full file contents use [Repomix](https://repomix.com).`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
if (opts.git) {
|
|
19
|
+
lines.push("## Git");
|
|
20
|
+
if (opts.git.branch)
|
|
21
|
+
lines.push(`- Branch: \`${opts.git.branch}\``);
|
|
22
|
+
if (opts.git.recentCommits.length) {
|
|
23
|
+
lines.push("- Recent commits:");
|
|
24
|
+
for (const c of opts.git.recentCommits)
|
|
25
|
+
lines.push(` - ${c}`);
|
|
26
|
+
}
|
|
27
|
+
if (opts.git.diffStat) {
|
|
28
|
+
lines.push("- Uncommitted diff stat:");
|
|
29
|
+
lines.push("```");
|
|
30
|
+
lines.push(opts.git.diffStat);
|
|
31
|
+
lines.push("```");
|
|
32
|
+
}
|
|
33
|
+
if (opts.git.changedFiles.length) {
|
|
34
|
+
lines.push("- Changed / untracked files:");
|
|
35
|
+
for (const f of opts.git.changedFiles)
|
|
36
|
+
lines.push(` - ${f}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("");
|
|
39
|
+
}
|
|
40
|
+
if (opts.manifests.length) {
|
|
41
|
+
lines.push("## Scripts & stack");
|
|
42
|
+
for (const m of opts.manifests) {
|
|
43
|
+
lines.push(`### ${m.kind}`);
|
|
44
|
+
if (m.extra?.length) {
|
|
45
|
+
for (const e of m.extra)
|
|
46
|
+
lines.push(`- ${e}`);
|
|
47
|
+
}
|
|
48
|
+
if (m.scripts && Object.keys(m.scripts).length) {
|
|
49
|
+
const keys = opts.compact
|
|
50
|
+
? ["dev", "build", "test", "start", "lint"].filter((k) => m.scripts[k])
|
|
51
|
+
: Object.keys(m.scripts);
|
|
52
|
+
for (const k of keys) {
|
|
53
|
+
lines.push(`- \`${k}\`: ${m.scripts[k]}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (m.deps?.length) {
|
|
57
|
+
lines.push(`- Key deps: ${m.deps.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (opts.snippets.length) {
|
|
63
|
+
lines.push("## Existing instructions");
|
|
64
|
+
lines.push(...opts.snippets);
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
lines.push("## Project tree");
|
|
68
|
+
lines.push("```");
|
|
69
|
+
lines.push(...opts.tree);
|
|
70
|
+
lines.push("```");
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push("---");
|
|
73
|
+
lines.push("Tip: re-run `pack_context` / `npx ctxshot` after major refactors.");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function isGitRepo(root: string): boolean;
|
|
2
|
+
export interface GitSummary {
|
|
3
|
+
branch: string | null;
|
|
4
|
+
recentCommits: string[];
|
|
5
|
+
diffStat: string | null;
|
|
6
|
+
changedFiles: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function gitSummary(root: string, withDiff: boolean): GitSummary | null;
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
function run(cmd, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
return execSync(cmd, {
|
|
5
|
+
cwd,
|
|
6
|
+
encoding: "utf8",
|
|
7
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8
|
+
}).trim();
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isGitRepo(root) {
|
|
15
|
+
return run("git rev-parse --is-inside-work-tree", root) === "true";
|
|
16
|
+
}
|
|
17
|
+
export function gitSummary(root, withDiff) {
|
|
18
|
+
if (!isGitRepo(root))
|
|
19
|
+
return null;
|
|
20
|
+
const branch = run("git branch --show-current", root);
|
|
21
|
+
const log = run("git log -5 --oneline", root);
|
|
22
|
+
const recentCommits = log ? log.split("\n").filter(Boolean) : [];
|
|
23
|
+
let diffStat = null;
|
|
24
|
+
let changedFiles = [];
|
|
25
|
+
if (withDiff) {
|
|
26
|
+
diffStat = run("git diff --stat", root);
|
|
27
|
+
const names = run("git diff --name-only", root);
|
|
28
|
+
changedFiles = names ? names.split("\n").filter(Boolean).slice(0, 30) : [];
|
|
29
|
+
const untracked = run("git ls-files --others --exclude-standard", root);
|
|
30
|
+
if (untracked) {
|
|
31
|
+
changedFiles.push(...untracked.split("\n").filter(Boolean).slice(0, 10));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { branch, recentCommits, diffStat, changedFiles };
|
|
35
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const DEFAULT_IGNORE = new Set([
|
|
4
|
+
".git",
|
|
5
|
+
"node_modules",
|
|
6
|
+
"dist",
|
|
7
|
+
"build",
|
|
8
|
+
".next",
|
|
9
|
+
".nuxt",
|
|
10
|
+
"coverage",
|
|
11
|
+
".turbo",
|
|
12
|
+
".cache",
|
|
13
|
+
"__pycache__",
|
|
14
|
+
".venv",
|
|
15
|
+
"venv",
|
|
16
|
+
".idea",
|
|
17
|
+
".vscode",
|
|
18
|
+
"target",
|
|
19
|
+
"vendor",
|
|
20
|
+
".ai",
|
|
21
|
+
]);
|
|
22
|
+
export function loadGitignore(root) {
|
|
23
|
+
const patterns = new Set();
|
|
24
|
+
const file = join(root, ".gitignore");
|
|
25
|
+
if (!existsSync(file))
|
|
26
|
+
return patterns;
|
|
27
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
28
|
+
for (const raw of lines) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (!line || line.startsWith("#"))
|
|
31
|
+
continue;
|
|
32
|
+
patterns.add(line.replace(/\/$/, ""));
|
|
33
|
+
}
|
|
34
|
+
return patterns;
|
|
35
|
+
}
|
|
36
|
+
function matchPattern(name, pattern) {
|
|
37
|
+
if (pattern.startsWith("*.")) {
|
|
38
|
+
return name.endsWith(pattern.slice(1));
|
|
39
|
+
}
|
|
40
|
+
if (pattern.includes("*")) {
|
|
41
|
+
const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
|
|
42
|
+
return re.test(name);
|
|
43
|
+
}
|
|
44
|
+
return (name === pattern ||
|
|
45
|
+
name.endsWith("/" + pattern) ||
|
|
46
|
+
name.startsWith(pattern + "/"));
|
|
47
|
+
}
|
|
48
|
+
export function shouldIgnore(relPath, name, gitignore, extra = DEFAULT_IGNORE) {
|
|
49
|
+
if (extra.has(name))
|
|
50
|
+
return true;
|
|
51
|
+
const parts = relPath.split(/[/\\]/);
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (extra.has(part))
|
|
54
|
+
return true;
|
|
55
|
+
for (const p of gitignore) {
|
|
56
|
+
if (matchPattern(part, p) || matchPattern(relPath, p))
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
export { DEFAULT_IGNORE };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { packContext, type PackOptions, type PackResult, type ContextStats } from "./pack.js";
|
|
2
|
+
export { formatBrief, estimateTokens, type FormatOptions } from "./format.js";
|
|
3
|
+
export { buildTree, readManifests, readContextSnippets, type TreeOptions, type ManifestSummary } from "./scan.js";
|
|
4
|
+
export { gitSummary, isGitRepo, type GitSummary } from "./git.js";
|
|
5
|
+
export { loadGitignore, shouldIgnore, DEFAULT_IGNORE } from "./ignore.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { packContext } from "./pack.js";
|
|
2
|
+
export { formatBrief, estimateTokens } from "./format.js";
|
|
3
|
+
export { buildTree, readManifests, readContextSnippets } from "./scan.js";
|
|
4
|
+
export { gitSummary, isGitRepo } from "./git.js";
|
|
5
|
+
export { loadGitignore, shouldIgnore, DEFAULT_IGNORE } from "./ignore.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PackOptions {
|
|
2
|
+
/** Project root (default: process.cwd()) */
|
|
3
|
+
root?: string;
|
|
4
|
+
compact?: boolean;
|
|
5
|
+
diff?: boolean;
|
|
6
|
+
depth?: number;
|
|
7
|
+
maxEntries?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ContextStats {
|
|
10
|
+
estimatedTokens: number;
|
|
11
|
+
lineCount: number;
|
|
12
|
+
treeLineCount: number;
|
|
13
|
+
compact: boolean;
|
|
14
|
+
hasGit: boolean;
|
|
15
|
+
manifestCount: number;
|
|
16
|
+
}
|
|
17
|
+
export interface PackResult {
|
|
18
|
+
markdown: string;
|
|
19
|
+
stats: ContextStats;
|
|
20
|
+
root: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function packContext(opts?: PackOptions): PackResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { buildTree, readManifests, readContextSnippets } from "./scan.js";
|
|
3
|
+
import { gitSummary } from "./git.js";
|
|
4
|
+
import { formatBrief, estimateTokens } from "./format.js";
|
|
5
|
+
export function packContext(opts = {}) {
|
|
6
|
+
const root = resolve(opts.root ?? process.cwd());
|
|
7
|
+
const compact = opts.compact ?? false;
|
|
8
|
+
const diff = opts.diff ?? false;
|
|
9
|
+
const depth = opts.depth ?? (compact ? 2 : 3);
|
|
10
|
+
const maxEntries = opts.maxEntries ?? (compact ? 50 : 80);
|
|
11
|
+
const tree = buildTree({ root, maxDepth: depth, maxEntries });
|
|
12
|
+
const manifests = readManifests(root);
|
|
13
|
+
const snippets = readContextSnippets(root, compact);
|
|
14
|
+
const git = gitSummary(root, diff);
|
|
15
|
+
const markdown = formatBrief({
|
|
16
|
+
root,
|
|
17
|
+
tree,
|
|
18
|
+
manifests,
|
|
19
|
+
snippets,
|
|
20
|
+
git,
|
|
21
|
+
compact,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
markdown,
|
|
25
|
+
root,
|
|
26
|
+
stats: {
|
|
27
|
+
estimatedTokens: estimateTokens(markdown),
|
|
28
|
+
lineCount: markdown.split("\n").length,
|
|
29
|
+
treeLineCount: tree.length,
|
|
30
|
+
compact,
|
|
31
|
+
hasGit: git !== null,
|
|
32
|
+
manifestCount: manifests.length,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TreeOptions {
|
|
2
|
+
root: string;
|
|
3
|
+
maxDepth: number;
|
|
4
|
+
maxEntries: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildTree(opts: TreeOptions): string[];
|
|
7
|
+
export interface ManifestSummary {
|
|
8
|
+
kind: string;
|
|
9
|
+
scripts?: Record<string, string>;
|
|
10
|
+
deps?: string[];
|
|
11
|
+
extra?: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function readManifests(root: string): ManifestSummary[];
|
|
14
|
+
export declare function readContextSnippets(root: string, compact: boolean): string[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { shouldIgnore, loadGitignore } from "./ignore.js";
|
|
4
|
+
export function buildTree(opts) {
|
|
5
|
+
const gitignore = loadGitignore(opts.root);
|
|
6
|
+
const lines = [];
|
|
7
|
+
let count = 0;
|
|
8
|
+
function walk(dir, depth, prefix) {
|
|
9
|
+
if (depth > opts.maxDepth || count >= opts.maxEntries)
|
|
10
|
+
return;
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const visible = entries.filter((name) => {
|
|
19
|
+
const rel = relative(opts.root, join(dir, name)).replace(/\\/g, "/");
|
|
20
|
+
return !shouldIgnore(rel, name, gitignore);
|
|
21
|
+
});
|
|
22
|
+
for (let i = 0; i < visible.length; i++) {
|
|
23
|
+
if (count >= opts.maxEntries) {
|
|
24
|
+
lines.push(`${prefix}… (${opts.maxEntries} entries max)`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const name = visible[i];
|
|
28
|
+
const full = join(dir, name);
|
|
29
|
+
const last = i === visible.length - 1;
|
|
30
|
+
const branch = last ? "└── " : "├── ";
|
|
31
|
+
const childPrefix = prefix + (last ? " " : "│ ");
|
|
32
|
+
let label = name;
|
|
33
|
+
try {
|
|
34
|
+
if (statSync(full).isDirectory()) {
|
|
35
|
+
label += "/";
|
|
36
|
+
lines.push(prefix + branch + label);
|
|
37
|
+
count++;
|
|
38
|
+
walk(full, depth + 1, childPrefix);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
lines.push(prefix + branch + label);
|
|
42
|
+
count++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
lines.push(prefix + branch + label);
|
|
47
|
+
count++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
lines.push(".");
|
|
52
|
+
walk(opts.root, 1, "");
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
export function readManifests(root) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const pkg = join(root, "package.json");
|
|
58
|
+
if (existsSync(pkg)) {
|
|
59
|
+
try {
|
|
60
|
+
const j = JSON.parse(readFileSync(pkg, "utf8"));
|
|
61
|
+
const deps = [
|
|
62
|
+
...Object.keys(j.dependencies ?? {}),
|
|
63
|
+
...Object.keys(j.devDependencies ?? {}),
|
|
64
|
+
].slice(0, 12);
|
|
65
|
+
out.push({
|
|
66
|
+
kind: `package.json${j.name ? ` (${j.name})` : ""}`,
|
|
67
|
+
scripts: j.scripts,
|
|
68
|
+
deps,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
out.push({ kind: "package.json (parse error)" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const py = join(root, "pyproject.toml");
|
|
76
|
+
if (existsSync(py)) {
|
|
77
|
+
const text = readFileSync(py, "utf8");
|
|
78
|
+
const scripts = {};
|
|
79
|
+
const match = text.match(/\[project\.scripts\]([\s\S]*?)(?:\[|$)/);
|
|
80
|
+
if (match) {
|
|
81
|
+
for (const line of match[1].split("\n")) {
|
|
82
|
+
const m = line.match(/^(\w[\w-]*)\s*=\s*"(.+)"\s*$/);
|
|
83
|
+
if (m)
|
|
84
|
+
scripts[m[1]] = m[2];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
out.push({
|
|
88
|
+
kind: "pyproject.toml",
|
|
89
|
+
scripts: Object.keys(scripts).length ? scripts : undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const go = join(root, "go.mod");
|
|
93
|
+
if (existsSync(go)) {
|
|
94
|
+
const first = readFileSync(go, "utf8").split("\n")[0];
|
|
95
|
+
out.push({ kind: "go.mod", extra: [first.trim()] });
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
const CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md", ".cursor/rules", "README.md"];
|
|
100
|
+
export function readContextSnippets(root, compact) {
|
|
101
|
+
const snippets = [];
|
|
102
|
+
const limit = compact ? 400 : 1200;
|
|
103
|
+
for (const rel of CONTEXT_FILES) {
|
|
104
|
+
const full = join(root, rel);
|
|
105
|
+
if (!existsSync(full))
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
const st = statSync(full);
|
|
109
|
+
if (st.isDirectory())
|
|
110
|
+
continue;
|
|
111
|
+
const text = readFileSync(full, "utf8").trim();
|
|
112
|
+
if (!text)
|
|
113
|
+
continue;
|
|
114
|
+
const body = text.length > limit ? text.slice(0, limit) + "\n…(truncated)" : text;
|
|
115
|
+
snippets.push(`### ${rel}\n\n${body}`);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* skip */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return snippets;
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ctxshot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pack your repo into a compact AI-ready context brief in 3 seconds — before every Claude / Cursor session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ctxshot": "dist/cli.js"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/cli.js",
|
|
11
|
+
"./core": "./dist/core/index.js"
|
|
12
|
+
},
|
|
9
13
|
"files": [
|
|
10
14
|
"dist",
|
|
11
15
|
"README.md",
|
|
@@ -17,7 +21,8 @@
|
|
|
17
21
|
"dev": "tsx src/cli.ts",
|
|
18
22
|
"prepublishOnly": "npm run build && npm run test:smoke",
|
|
19
23
|
"test": "node --test",
|
|
20
|
-
"test:smoke": "node scripts/ci-smoke.mjs"
|
|
24
|
+
"test:smoke": "node scripts/ci-smoke.mjs",
|
|
25
|
+
"benchmark": "node scripts/benchmark.mjs"
|
|
21
26
|
},
|
|
22
27
|
"keywords": [
|
|
23
28
|
"ai",
|
|
@@ -26,7 +31,8 @@
|
|
|
26
31
|
"context",
|
|
27
32
|
"developer-tools",
|
|
28
33
|
"llm",
|
|
29
|
-
"productivity"
|
|
34
|
+
"productivity",
|
|
35
|
+
"mcp"
|
|
30
36
|
],
|
|
31
37
|
"license": "MIT",
|
|
32
38
|
"engines": {
|