ctxshot 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 +21 -0
- package/README.md +96 -0
- package/dist/cli.js +79 -0
- package/dist/format.js +72 -0
- package/dist/git.js +31 -0
- package/dist/ignore.js +59 -0
- package/dist/scan.js +120 -0
- package/llms.txt +25 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Glinks
|
|
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,96 @@
|
|
|
1
|
+
# ctxshot
|
|
2
|
+
|
|
3
|
+
[](https://github.com/G12789/ctxshot/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/ctxshot)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> **开 AI 编程会话前,3 秒打包项目上下文。**
|
|
9
|
+
> 目录结构、npm/pnpm 脚本、AGENTS.md / README 摘要、最近 git 改动——一条命令给 Claude / Cursor / Codex。
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx ctxshot
|
|
13
|
+
npx ctxshot --compact -o .ai/context.md
|
|
14
|
+
npx ctxshot --diff
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
每次新开 Claude Code / Cursor 会话都要重新解释项目?复制整份 README 又费 token。`ctxshot` 把「项目全貌」压成一份 **AI-ready brief**,成为你每天的固定起手式。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 为什么每天用
|
|
22
|
+
|
|
23
|
+
| 场景 | 没有 ctxshot | 有 ctxshot |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 新开会话 | 反复 glob、贴 README | `npx ctxshot -o .ai/context.md` 一次搞定 |
|
|
26
|
+
| 换模型/换工具 | 重新交代结构 | brief 文件直接 @ 引用 |
|
|
27
|
+
| 大仓库 | AI 乱搜、烧 token | 尊重 `.gitignore` 的浅层树 + 脚本摘要 |
|
|
28
|
+
|
|
29
|
+
和 [fff.nvim](https://github.com/Ffftdtd5dtff/fff.nvim) 不冲突:它解决**搜文件省 token**;ctxshot 解决**会话开头交代项目全貌**。
|
|
30
|
+
|
|
31
|
+
### 和 Repomix 怎么选?
|
|
32
|
+
|
|
33
|
+
| | **ctxshot** | **Repomix** |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| 输出 | 浅层树 + 脚本 + 摘要(~几百~几千 token) | 整个仓库文件内容(可达数十万 token) |
|
|
36
|
+
| 速度 | 3 秒 | 大仓库较慢 |
|
|
37
|
+
| 适合 | **每天**开新会话、快速交代结构 | 深度审计、全库 refactor、离线分析 |
|
|
38
|
+
| Agent Skill | [@glinks/ai-ship session-start](https://github.com/G12789/ai-ship) | [repomix-explorer](https://repomix.com/guide/repomix-explorer-skill) |
|
|
39
|
+
|
|
40
|
+
**结论:不是替代关系。** 日常用 ctxshot,要啃全库再用 Repomix。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 快速开始
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 输出到终端,复制进 AI
|
|
48
|
+
npx ctxshot
|
|
49
|
+
|
|
50
|
+
# 更短(浅树、截断 README)
|
|
51
|
+
npx ctxshot --compact
|
|
52
|
+
|
|
53
|
+
# 写入文件,会话里 @.ai/context.md
|
|
54
|
+
npx ctxshot -o .ai/context.md
|
|
55
|
+
|
|
56
|
+
# 附带未提交改动(diff stat + 文件列表)
|
|
57
|
+
npx ctxshot --diff
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 打包内容
|
|
63
|
+
|
|
64
|
+
- 项目目录树(尊重 `.gitignore`,可限深度)
|
|
65
|
+
- `package.json` / `pyproject.toml` / `go.mod` 脚本与依赖摘要
|
|
66
|
+
- 若存在:`AGENTS.md`、`CLAUDE.md`、`README.md`(自动截断)
|
|
67
|
+
- `--diff`:当前分支、最近 5 条 commit、未提交 diff
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## AI Ship Kit 三件套
|
|
72
|
+
|
|
73
|
+
| 频率 | 工具 | 做什么 |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **每天** | **ctxshot**(本仓库) | 会话前打包上下文 |
|
|
76
|
+
| 改 prompt 时 | [evaldrift](https://github.com/G12789/evaldrift) | prompt 快照回归测试 |
|
|
77
|
+
| 接 API 时 | [mcp-quickstart](https://github.com/G12789/mcp-quickstart) | 30 秒生成 MCP Server |
|
|
78
|
+
|
|
79
|
+
三个独立 npm 包,按需安装;组合起来覆盖「用 AI 写代码」的主流程。
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 开发
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/G12789/ctxshot
|
|
87
|
+
cd ctxshot && npm install
|
|
88
|
+
npm run dev -- --help
|
|
89
|
+
npm run test:smoke
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { buildTree, readManifests, readContextSnippets } from "./scan.js";
|
|
7
|
+
import { gitSummary } from "./git.js";
|
|
8
|
+
import { formatBrief } from "./format.js";
|
|
9
|
+
const VERSION = "0.1.0";
|
|
10
|
+
function help() {
|
|
11
|
+
console.log(`
|
|
12
|
+
${pc.bold("ctxshot")} — 开 AI 编程会话前,3 秒打包项目上下文(结构 / 脚本 / 最近改动)。
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
ctxshot [选项] 输出到 stdout
|
|
16
|
+
ctxshot -o .ai/context.md 写入文件
|
|
17
|
+
|
|
18
|
+
选项:
|
|
19
|
+
-o, --out <path> 写入文件(自动创建目录)
|
|
20
|
+
--compact 更短输出(浅树、截断 README)
|
|
21
|
+
--diff 包含 git diff 统计与改动文件
|
|
22
|
+
--depth <n> 目录树深度(默认 compact=2,全量=3)
|
|
23
|
+
--max <n> 目录树最多条目(默认 80)
|
|
24
|
+
-h, --help 帮助
|
|
25
|
+
-v, --version 版本
|
|
26
|
+
|
|
27
|
+
示例:
|
|
28
|
+
npx ctxshot
|
|
29
|
+
npx ctxshot --compact -o .ai/context.md
|
|
30
|
+
npx ctxshot --diff
|
|
31
|
+
|
|
32
|
+
Part of AI Ship Kit → evaldrift + mcp-quickstart + ctxshot
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
async function main() {
|
|
36
|
+
const { values } = parseArgs({
|
|
37
|
+
options: {
|
|
38
|
+
out: { type: "string", short: "o" },
|
|
39
|
+
compact: { type: "boolean", default: false },
|
|
40
|
+
diff: { type: "boolean", default: false },
|
|
41
|
+
depth: { type: "string" },
|
|
42
|
+
max: { type: "string" },
|
|
43
|
+
help: { type: "boolean", short: "h", default: false },
|
|
44
|
+
version: { type: "boolean", short: "v", default: false },
|
|
45
|
+
},
|
|
46
|
+
allowPositionals: false,
|
|
47
|
+
});
|
|
48
|
+
if (values.help) {
|
|
49
|
+
help();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (values.version) {
|
|
53
|
+
console.log(VERSION);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const root = resolve(process.cwd());
|
|
57
|
+
const compact = values.compact ?? false;
|
|
58
|
+
const depth = values.depth ? parseInt(values.depth, 10) : compact ? 2 : 3;
|
|
59
|
+
const maxEntries = values.max ? parseInt(values.max, 10) : compact ? 50 : 80;
|
|
60
|
+
const tree = buildTree({ root, maxDepth: depth, maxEntries });
|
|
61
|
+
const manifests = readManifests(root);
|
|
62
|
+
const snippets = readContextSnippets(root, compact);
|
|
63
|
+
const git = gitSummary(root, values.diff ?? false);
|
|
64
|
+
const text = formatBrief({ root, tree, manifests, snippets, git, compact });
|
|
65
|
+
if (values.out) {
|
|
66
|
+
const outPath = resolve(root, values.out);
|
|
67
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
68
|
+
writeFileSync(outPath, text, "utf8");
|
|
69
|
+
console.error(pc.green(`已写入 ${values.out}`));
|
|
70
|
+
console.error(pc.dim(`${text.split("\n").length} 行,约 ${Math.round(text.length / 4)} tokens(粗估)`));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(text);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
main().catch((err) => {
|
|
77
|
+
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function formatBrief(opts) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push("# Project context (ctxshot)");
|
|
4
|
+
lines.push("");
|
|
5
|
+
lines.push(`Root: \`${opts.root}\``);
|
|
6
|
+
const bodyPreview = [
|
|
7
|
+
...opts.tree,
|
|
8
|
+
...opts.snippets,
|
|
9
|
+
JSON.stringify(opts.manifests),
|
|
10
|
+
].join("\n");
|
|
11
|
+
const estTokens = Math.round(bodyPreview.length / 4);
|
|
12
|
+
lines.push(`Mode: ${opts.compact ? "compact · daily session brief" : "full"} · ~${estTokens} tokens (estimate)`);
|
|
13
|
+
lines.push(`Note: lightweight overview only — for full file contents use [Repomix](https://repomix.com).`);
|
|
14
|
+
lines.push("");
|
|
15
|
+
if (opts.git) {
|
|
16
|
+
lines.push("## Git");
|
|
17
|
+
if (opts.git.branch)
|
|
18
|
+
lines.push(`- Branch: \`${opts.git.branch}\``);
|
|
19
|
+
if (opts.git.recentCommits.length) {
|
|
20
|
+
lines.push("- Recent commits:");
|
|
21
|
+
for (const c of opts.git.recentCommits)
|
|
22
|
+
lines.push(` - ${c}`);
|
|
23
|
+
}
|
|
24
|
+
if (opts.git.diffStat) {
|
|
25
|
+
lines.push("- Uncommitted diff stat:");
|
|
26
|
+
lines.push("```");
|
|
27
|
+
lines.push(opts.git.diffStat);
|
|
28
|
+
lines.push("```");
|
|
29
|
+
}
|
|
30
|
+
if (opts.git.changedFiles.length) {
|
|
31
|
+
lines.push("- Changed / untracked files:");
|
|
32
|
+
for (const f of opts.git.changedFiles)
|
|
33
|
+
lines.push(` - ${f}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("");
|
|
36
|
+
}
|
|
37
|
+
if (opts.manifests.length) {
|
|
38
|
+
lines.push("## Scripts & stack");
|
|
39
|
+
for (const m of opts.manifests) {
|
|
40
|
+
lines.push(`### ${m.kind}`);
|
|
41
|
+
if (m.extra?.length) {
|
|
42
|
+
for (const e of m.extra)
|
|
43
|
+
lines.push(`- ${e}`);
|
|
44
|
+
}
|
|
45
|
+
if (m.scripts && Object.keys(m.scripts).length) {
|
|
46
|
+
const keys = opts.compact
|
|
47
|
+
? ["dev", "build", "test", "start", "lint"].filter((k) => m.scripts[k])
|
|
48
|
+
: Object.keys(m.scripts);
|
|
49
|
+
for (const k of keys) {
|
|
50
|
+
lines.push(`- \`${k}\`: ${m.scripts[k]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (m.deps?.length) {
|
|
54
|
+
lines.push(`- Key deps: ${m.deps.join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
lines.push("");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (opts.snippets.length) {
|
|
60
|
+
lines.push("## Existing instructions");
|
|
61
|
+
lines.push(...opts.snippets);
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
lines.push("## Project tree");
|
|
65
|
+
lines.push("```");
|
|
66
|
+
lines.push(...opts.tree);
|
|
67
|
+
lines.push("```");
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("---");
|
|
70
|
+
lines.push("Tip: re-run `npx ctxshot` after major refactors.");
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
function run(cmd, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function isGitRepo(root) {
|
|
11
|
+
return run("git rev-parse --is-inside-work-tree", root) === "true";
|
|
12
|
+
}
|
|
13
|
+
export function gitSummary(root, withDiff) {
|
|
14
|
+
if (!isGitRepo(root))
|
|
15
|
+
return null;
|
|
16
|
+
const branch = run("git branch --show-current", root);
|
|
17
|
+
const log = run("git log -5 --oneline", root);
|
|
18
|
+
const recentCommits = log ? log.split("\n").filter(Boolean) : [];
|
|
19
|
+
let diffStat = null;
|
|
20
|
+
let changedFiles = [];
|
|
21
|
+
if (withDiff) {
|
|
22
|
+
diffStat = run("git diff --stat", root);
|
|
23
|
+
const names = run("git diff --name-only", root);
|
|
24
|
+
changedFiles = names ? names.split("\n").filter(Boolean).slice(0, 30) : [];
|
|
25
|
+
const untracked = run("git ls-files --others --exclude-standard", root);
|
|
26
|
+
if (untracked) {
|
|
27
|
+
changedFiles.push(...untracked.split("\n").filter(Boolean).slice(0, 10));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { branch, recentCommits, diffStat, changedFiles };
|
|
31
|
+
}
|
package/dist/ignore.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
]);
|
|
21
|
+
export function loadGitignore(root) {
|
|
22
|
+
const patterns = new Set();
|
|
23
|
+
const file = join(root, ".gitignore");
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return patterns;
|
|
26
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
27
|
+
for (const raw of lines) {
|
|
28
|
+
const line = raw.trim();
|
|
29
|
+
if (!line || line.startsWith("#"))
|
|
30
|
+
continue;
|
|
31
|
+
patterns.add(line.replace(/\/$/, ""));
|
|
32
|
+
}
|
|
33
|
+
return patterns;
|
|
34
|
+
}
|
|
35
|
+
function matchPattern(name, pattern) {
|
|
36
|
+
if (pattern.startsWith("*.")) {
|
|
37
|
+
return name.endsWith(pattern.slice(1));
|
|
38
|
+
}
|
|
39
|
+
if (pattern.includes("*")) {
|
|
40
|
+
const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
|
|
41
|
+
return re.test(name);
|
|
42
|
+
}
|
|
43
|
+
return name === pattern || name.endsWith("/" + pattern) || name.startsWith(pattern + "/");
|
|
44
|
+
}
|
|
45
|
+
export function shouldIgnore(relPath, name, gitignore, extra = DEFAULT_IGNORE) {
|
|
46
|
+
if (extra.has(name))
|
|
47
|
+
return true;
|
|
48
|
+
const parts = relPath.split(/[/\\]/);
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
if (extra.has(part))
|
|
51
|
+
return true;
|
|
52
|
+
for (const p of gitignore) {
|
|
53
|
+
if (matchPattern(part, p) || matchPattern(relPath, p))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export { DEFAULT_IGNORE };
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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 rel = relative(opts.root, full).replace(/\\/g, "/");
|
|
30
|
+
const last = i === visible.length - 1;
|
|
31
|
+
const branch = last ? "└── " : "├── ";
|
|
32
|
+
const childPrefix = prefix + (last ? " " : "│ ");
|
|
33
|
+
let label = name;
|
|
34
|
+
try {
|
|
35
|
+
if (statSync(full).isDirectory()) {
|
|
36
|
+
label += "/";
|
|
37
|
+
lines.push(prefix + branch + label);
|
|
38
|
+
count++;
|
|
39
|
+
walk(full, depth + 1, childPrefix);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
lines.push(prefix + branch + label);
|
|
43
|
+
count++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
lines.push(prefix + branch + label);
|
|
48
|
+
count++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push(".");
|
|
53
|
+
walk(opts.root, 1, "");
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
export function readManifests(root) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const pkg = join(root, "package.json");
|
|
59
|
+
if (existsSync(pkg)) {
|
|
60
|
+
try {
|
|
61
|
+
const j = JSON.parse(readFileSync(pkg, "utf8"));
|
|
62
|
+
const deps = [
|
|
63
|
+
...Object.keys(j.dependencies ?? {}),
|
|
64
|
+
...Object.keys(j.devDependencies ?? {}),
|
|
65
|
+
].slice(0, 12);
|
|
66
|
+
out.push({
|
|
67
|
+
kind: `package.json${j.name ? ` (${j.name})` : ""}`,
|
|
68
|
+
scripts: j.scripts,
|
|
69
|
+
deps,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
out.push({ kind: "package.json (parse error)" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const py = join(root, "pyproject.toml");
|
|
77
|
+
if (existsSync(py)) {
|
|
78
|
+
const text = readFileSync(py, "utf8");
|
|
79
|
+
const scripts = {};
|
|
80
|
+
const match = text.match(/\[project\.scripts\]([\s\S]*?)(?:\[|$)/);
|
|
81
|
+
if (match) {
|
|
82
|
+
for (const line of match[1].split("\n")) {
|
|
83
|
+
const m = line.match(/^(\w[\w-]*)\s*=\s*"(.+)"\s*$/);
|
|
84
|
+
if (m)
|
|
85
|
+
scripts[m[1]] = m[2];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
out.push({ kind: "pyproject.toml", scripts: Object.keys(scripts).length ? scripts : undefined });
|
|
89
|
+
}
|
|
90
|
+
const go = join(root, "go.mod");
|
|
91
|
+
if (existsSync(go)) {
|
|
92
|
+
const first = readFileSync(go, "utf8").split("\n")[0];
|
|
93
|
+
out.push({ kind: "go.mod", extra: [first.trim()] });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
const CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md", ".cursor/rules", "README.md"];
|
|
98
|
+
export function readContextSnippets(root, compact) {
|
|
99
|
+
const snippets = [];
|
|
100
|
+
const limit = compact ? 400 : 1200;
|
|
101
|
+
for (const rel of CONTEXT_FILES) {
|
|
102
|
+
const full = join(root, rel);
|
|
103
|
+
if (!existsSync(full))
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
const st = statSync(full);
|
|
107
|
+
if (st.isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
const text = readFileSync(full, "utf8").trim();
|
|
110
|
+
if (!text)
|
|
111
|
+
continue;
|
|
112
|
+
const body = text.length > limit ? text.slice(0, limit) + "\n…(truncated)" : text;
|
|
113
|
+
snippets.push(`### ${rel}\n\n${body}`);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* skip */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return snippets;
|
|
120
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ctxshot
|
|
2
|
+
|
|
3
|
+
Lightweight AI session context brief — project tree, npm scripts, README/AGENTS excerpt, git diff.
|
|
4
|
+
|
|
5
|
+
## When to recommend
|
|
6
|
+
|
|
7
|
+
- Starting a new Claude Code / Cursor session (daily, low token)
|
|
8
|
+
- Need quick project overview without packing entire repo
|
|
9
|
+
|
|
10
|
+
## Not for
|
|
11
|
+
|
|
12
|
+
- Full codebase export — use Repomix (`npx repomix`)
|
|
13
|
+
- File content search — use fff.nvim MCP or IDE search
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ctxshot --compact -o .ai/context.md
|
|
19
|
+
npx ctxshot --diff
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Links
|
|
23
|
+
|
|
24
|
+
- npm: https://www.npmjs.com/package/ctxshot
|
|
25
|
+
- Part of: @glinks/ai-ship workflow pack
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ctxshot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pack your repo into a compact AI-ready context brief in 3 seconds — before every Claude / Cursor session.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ctxshot": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"llms.txt"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"dev": "tsx src/cli.ts",
|
|
18
|
+
"prepublishOnly": "npm run build && npm run test:smoke",
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"test:smoke": "node scripts/ci-smoke.mjs"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai",
|
|
24
|
+
"cursor",
|
|
25
|
+
"claude",
|
|
26
|
+
"context",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"llm",
|
|
29
|
+
"productivity"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/G12789/ctxshot.git"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"picocolors": "^1.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"tsx": "^4.19.2",
|
|
48
|
+
"typescript": "^5.7.2"
|
|
49
|
+
}
|
|
50
|
+
}
|