@cliperhq/cliper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +266 -0
  2. package/dist/commands/analyze.d.ts +6 -0
  3. package/dist/commands/analyze.d.ts.map +1 -0
  4. package/dist/commands/analyze.js +216 -0
  5. package/dist/commands/export.d.ts +6 -0
  6. package/dist/commands/export.d.ts.map +1 -0
  7. package/dist/commands/export.js +64 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.js +173 -0
  11. package/dist/commands/scope.d.ts +2 -0
  12. package/dist/commands/scope.d.ts.map +1 -0
  13. package/dist/commands/scope.js +124 -0
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.d.ts.map +1 -0
  16. package/dist/commands/status.js +100 -0
  17. package/dist/commands/sync.d.ts +6 -0
  18. package/dist/commands/sync.d.ts.map +1 -0
  19. package/dist/commands/sync.js +83 -0
  20. package/dist/context/builder.d.ts +19 -0
  21. package/dist/context/builder.d.ts.map +1 -0
  22. package/dist/context/builder.js +143 -0
  23. package/dist/gaps/detector.d.ts +10 -0
  24. package/dist/gaps/detector.d.ts.map +1 -0
  25. package/dist/gaps/detector.js +139 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +48 -0
  29. package/dist/resolver/urlFetcher.d.ts +9 -0
  30. package/dist/resolver/urlFetcher.d.ts.map +1 -0
  31. package/dist/resolver/urlFetcher.js +134 -0
  32. package/dist/scanner/dependencies.d.ts +14 -0
  33. package/dist/scanner/dependencies.d.ts.map +1 -0
  34. package/dist/scanner/dependencies.js +199 -0
  35. package/dist/scanner/fileContent.d.ts +8 -0
  36. package/dist/scanner/fileContent.d.ts.map +1 -0
  37. package/dist/scanner/fileContent.js +133 -0
  38. package/dist/scanner/fileTree.d.ts +2 -0
  39. package/dist/scanner/fileTree.d.ts.map +1 -0
  40. package/dist/scanner/fileTree.js +152 -0
  41. package/dist/scanner/gitContext.d.ts +19 -0
  42. package/dist/scanner/gitContext.d.ts.map +1 -0
  43. package/dist/scanner/gitContext.js +60 -0
  44. package/dist/scope/autoScope.d.ts +2 -0
  45. package/dist/scope/autoScope.d.ts.map +1 -0
  46. package/dist/scope/autoScope.js +226 -0
  47. package/dist/scope/config.d.ts +10 -0
  48. package/dist/scope/config.d.ts.map +1 -0
  49. package/dist/scope/config.js +71 -0
  50. package/index.js +2 -0
  51. package/package.json +37 -0
  52. package/src/commands/analyze.ts +201 -0
  53. package/src/commands/export.ts +33 -0
  54. package/src/commands/init.ts +174 -0
  55. package/src/commands/scope.ts +77 -0
  56. package/src/commands/status.ts +67 -0
  57. package/src/commands/sync.ts +51 -0
  58. package/src/context/builder.ts +178 -0
  59. package/src/gaps/detector.ts +131 -0
  60. package/src/index.ts +54 -0
  61. package/src/resolver/urlFetcher.ts +119 -0
  62. package/src/scanner/dependencies.ts +196 -0
  63. package/src/scanner/fileContent.ts +121 -0
  64. package/src/scanner/fileTree.ts +149 -0
  65. package/src/scanner/gitContext.ts +74 -0
  66. package/src/scope/autoScope.ts +182 -0
  67. package/src/scope/config.ts +39 -0
  68. package/tsconfig.json +19 -0
@@ -0,0 +1,196 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { FileContent } from "./fileContent";
4
+
5
+ export interface DependencyEdge {
6
+ from: string;
7
+ to: string;
8
+ type: "import" | "require" | "use" | "mod";
9
+ }
10
+
11
+ export interface DependencyMap {
12
+ edges: DependencyEdge[];
13
+ externalPackages: string[];
14
+ entryPoints: string[];
15
+ }
16
+
17
+ function extractJSImports(file: FileContent): { internal: string[]; external: string[] } {
18
+ const internal: string[] = [];
19
+ const external: string[] = [];
20
+
21
+ const patterns = [
22
+ /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
23
+ /require\(['"]([^'"]+)['"]\)/g,
24
+ /import\(['"]([^'"]+)['"]\)/g,
25
+ ];
26
+
27
+ for (const pattern of patterns) {
28
+ let match;
29
+ while ((match = pattern.exec(file.content)) !== null) {
30
+ const dep = match[1];
31
+ if (dep.startsWith(".") || dep.startsWith("/")) {
32
+ const dir = path.dirname(file.relativePath);
33
+ const resolved = path.normalize(path.join(dir, dep));
34
+ internal.push(resolved);
35
+ } else {
36
+ const pkgName = dep.startsWith("@")
37
+ ? dep.split("/").slice(0, 2).join("/")
38
+ : dep.split("/")[0];
39
+ external.push(pkgName);
40
+ }
41
+ }
42
+ }
43
+
44
+ return { internal, external };
45
+ }
46
+
47
+ function extractRustImports(file: FileContent): { internal: string[]; external: string[] } {
48
+ const internal: string[] = [];
49
+ const external: string[] = [];
50
+
51
+ const usePattern = /^use\s+([\w:]+)/gm;
52
+ const modPattern = /^(?:pub\s+)?mod\s+(\w+)/gm;
53
+
54
+ let match;
55
+ while ((match = usePattern.exec(file.content)) !== null) {
56
+ const dep = match[1];
57
+ if (dep.startsWith("crate::") || dep.startsWith("super::") || dep.startsWith("self::")) {
58
+ internal.push(dep.replace(/^(crate|super|self)::/, ""));
59
+ } else {
60
+ external.push(dep.split("::")[0]);
61
+ }
62
+ }
63
+
64
+ while ((match = modPattern.exec(file.content)) !== null) {
65
+ internal.push(match[1]);
66
+ }
67
+
68
+ return { internal, external };
69
+ }
70
+
71
+ function extractPythonImports(file: FileContent): { internal: string[]; external: string[] } {
72
+ const internal: string[] = [];
73
+ const external: string[] = [];
74
+
75
+ const relPattern = /^from\s+(\.+[\w.]*)\s+import/gm;
76
+ const absPatterns = [
77
+ /^import\s+([\w.]+)/gm,
78
+ /^from\s+([\w.]+)\s+import/gm,
79
+ ];
80
+
81
+ let match;
82
+ while ((match = relPattern.exec(file.content)) !== null) {
83
+ internal.push(match[1]);
84
+ }
85
+
86
+ for (const pattern of absPatterns) {
87
+ while ((match = pattern.exec(file.content)) !== null) {
88
+ external.push(match[1].split(".")[0]);
89
+ }
90
+ }
91
+
92
+ return { internal, external };
93
+ }
94
+
95
+ function getExtractor(filePath: string) {
96
+ const ext = path.extname(filePath).toLowerCase();
97
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs"].includes(ext)) return extractJSImports;
98
+ if (ext === ".rs") return extractRustImports;
99
+ if (ext === ".py") return extractPythonImports;
100
+ return null;
101
+ }
102
+
103
+ function isEntryPoint(filePath: string): boolean {
104
+ const name = path.basename(filePath);
105
+ return [
106
+ "index.ts", "index.js", "main.ts", "main.js",
107
+ "main.rs", "lib.rs",
108
+ "main.py", "__init__.py",
109
+ "main.go",
110
+ ].includes(name);
111
+ }
112
+
113
+ export function buildDependencyMap(files: FileContent[]): DependencyMap {
114
+ const edges: DependencyEdge[] = [];
115
+ const externalSet = new Set<string>();
116
+ const entryPoints: string[] = [];
117
+
118
+ for (const file of files) {
119
+ if (file.truncated) continue;
120
+
121
+ const extractor = getExtractor(file.relativePath);
122
+ if (!extractor) continue;
123
+
124
+ if (isEntryPoint(file.relativePath)) {
125
+ entryPoints.push(file.relativePath);
126
+ }
127
+
128
+ const { internal, external } = extractor(file);
129
+
130
+ for (const dep of internal) {
131
+ edges.push({
132
+ from: file.relativePath,
133
+ to: dep,
134
+ type: file.relativePath.endsWith(".rs") ? "use" : "import",
135
+ });
136
+ }
137
+
138
+ for (const pkg of external) {
139
+ externalSet.add(pkg);
140
+ }
141
+ }
142
+
143
+ const seen = new Set<string>();
144
+ const uniqueEdges = edges.filter((e) => {
145
+ const key = `${e.from}→${e.to}`;
146
+ if (seen.has(key)) return false;
147
+ seen.add(key);
148
+ return true;
149
+ });
150
+
151
+ return {
152
+ edges: uniqueEdges,
153
+ externalPackages: Array.from(externalSet).sort(),
154
+ entryPoints,
155
+ };
156
+ }
157
+
158
+ export function formatDependencyMap(map: DependencyMap): string {
159
+ if (map.edges.length === 0 && map.externalPackages.length === 0) {
160
+ return "No dependency information available for scoped files.";
161
+ }
162
+
163
+ const lines: string[] = [];
164
+
165
+ if (map.entryPoints.length > 0) {
166
+ lines.push("**Entry points:**");
167
+ for (const ep of map.entryPoints) {
168
+ lines.push(` → ${ep}`);
169
+ }
170
+ lines.push("");
171
+ }
172
+
173
+ if (map.edges.length > 0) {
174
+ lines.push("**Internal dependencies:**");
175
+ const byFile = new Map<string, string[]>();
176
+ for (const edge of map.edges) {
177
+ if (!byFile.has(edge.from)) byFile.set(edge.from, []);
178
+ byFile.get(edge.from)!.push(edge.to);
179
+ }
180
+ for (const [from, deps] of byFile) {
181
+ lines.push(` ${from}`);
182
+ for (const dep of deps.slice(0, 8)) {
183
+ lines.push(` ↳ ${dep}`);
184
+ }
185
+ if (deps.length > 8) lines.push(` ↳ ... and ${deps.length - 8} more`);
186
+ }
187
+ lines.push("");
188
+ }
189
+
190
+ if (map.externalPackages.length > 0) {
191
+ lines.push("**External packages used:**");
192
+ lines.push(` ${map.externalPackages.join(", ")}`);
193
+ }
194
+
195
+ return lines.join("\n");
196
+ }
@@ -0,0 +1,121 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { glob } from "glob";
4
+ import ignore from "ignore";
5
+
6
+
7
+ const MAX_TOTAL_CHARS = 150_000;
8
+
9
+ const BINARY_EXTENSIONS = new Set([
10
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp",
11
+ ".pdf", ".zip", ".tar", ".gz", ".exe", ".bin", ".wasm",
12
+ ".ttf", ".woff", ".woff2", ".eot", ".mp4", ".mp3",
13
+ ]);
14
+
15
+ const PRIORITY_EXTENSIONS = [
16
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".go",
17
+ ".java", ".kt", ".swift", ".rb", ".php", ".cs",
18
+ ".json", ".yaml", ".yml", ".toml", ".env.example",
19
+ ".md", ".mdx", ".sql", ".graphql", ".prisma",
20
+ ];
21
+
22
+ function isBinary(filePath: string): boolean {
23
+ const ext = path.extname(filePath).toLowerCase();
24
+ return BINARY_EXTENSIONS.has(ext);
25
+ }
26
+
27
+ function getPriority(filePath: string): number {
28
+ const ext = path.extname(filePath).toLowerCase();
29
+ const idx = PRIORITY_EXTENSIONS.indexOf(ext);
30
+ return idx === -1 ? 999 : idx;
31
+ }
32
+
33
+
34
+
35
+ export interface FileContent {
36
+ relativePath: string;
37
+ content: string;
38
+ truncated: boolean;
39
+ size: number;
40
+ }
41
+
42
+ export async function extractFileContents(
43
+ projectRoot: string,
44
+ activeScope: string[],
45
+ watchedScope: string[],
46
+ maxFileSizeKB: number = 50
47
+ ): Promise<FileContent[]> {
48
+ const MAX_FILE_SIZE_BYTES = maxFileSizeKB * 1024;
49
+ const ig = ignore();
50
+ const gitignorePath = path.join(projectRoot, ".gitignore");
51
+ if (fs.existsSync(gitignorePath)) {
52
+ ig.add(fs.readFileSync(gitignorePath, "utf-8"));
53
+ }
54
+
55
+ const allPaths = new Set<string>();
56
+
57
+ // Collect files from active scope and watched scope
58
+ for (const scopePath of [...activeScope, ...watchedScope]) {
59
+ const fullScopePath = path.join(projectRoot, scopePath);
60
+ if (!fs.existsSync(fullScopePath)) continue;
61
+
62
+ const stat = fs.statSync(fullScopePath);
63
+ if (stat.isFile()) {
64
+ allPaths.add(scopePath);
65
+ } else if (stat.isDirectory()) {
66
+ const files = await glob("**/*", {
67
+ cwd: fullScopePath,
68
+ nodir: true,
69
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
70
+ });
71
+ for (const f of files) {
72
+ const rel = path.join(scopePath, f);
73
+ if (!ig.ignores(rel)) allPaths.add(rel);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Sort by priority
79
+ const sortedPaths = Array.from(allPaths).sort(
80
+ (a, b) => getPriority(a) - getPriority(b)
81
+ );
82
+
83
+ const results: FileContent[] = [];
84
+ let totalChars = 0;
85
+
86
+ for (const relativePath of sortedPaths) {
87
+ if (totalChars >= MAX_TOTAL_CHARS) break;
88
+ if (isBinary(relativePath)) continue;
89
+
90
+ const fullPath = path.join(projectRoot, relativePath);
91
+ if (!fs.existsSync(fullPath)) continue;
92
+
93
+ const stat = fs.statSync(fullPath);
94
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
95
+ results.push({
96
+ relativePath,
97
+ content: `[File too large to include: ${Math.round(stat.size / 1024)}KB]`,
98
+ truncated: true,
99
+ size: stat.size,
100
+ });
101
+ continue;
102
+ }
103
+
104
+ try {
105
+ let content = fs.readFileSync(fullPath, "utf-8");
106
+ let truncated = false;
107
+
108
+ if (totalChars + content.length > MAX_TOTAL_CHARS) {
109
+ content = content.slice(0, MAX_TOTAL_CHARS - totalChars);
110
+ truncated = true;
111
+ }
112
+
113
+ totalChars += content.length;
114
+ results.push({ relativePath, content, truncated, size: stat.size });
115
+ } catch {
116
+ // Skip files that can't be read
117
+ }
118
+ }
119
+
120
+ return results;
121
+ }
@@ -0,0 +1,149 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import ignore from "ignore";
4
+
5
+ const IGNORE_DIRS = new Set([
6
+ "node_modules", ".git", "dist", "build", ".next", ".nuxt",
7
+ "coverage", ".cache", "__pycache__", ".pytest_cache", "vendor",
8
+ ".cliper",
9
+ ]);
10
+
11
+ interface TreeNode {
12
+ name: string;
13
+ isDir: boolean;
14
+ children?: TreeNode[];
15
+ inScope: boolean;
16
+ watched: boolean;
17
+ relativePath: string;
18
+ modifiedAt?: Date;
19
+ }
20
+
21
+ function loadGitignore(projectRoot: string) {
22
+ const ig = ignore();
23
+ const gitignorePath = path.join(projectRoot, ".gitignore");
24
+ if (fs.existsSync(gitignorePath)) {
25
+ ig.add(fs.readFileSync(gitignorePath, "utf-8"));
26
+ }
27
+ return ig;
28
+ }
29
+
30
+ function getModifiedTime(filePath: string): Date | undefined {
31
+ try {
32
+ return fs.statSync(filePath).mtime;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ function buildTree(
39
+ dirPath: string,
40
+ projectRoot: string,
41
+ activeScope: string[],
42
+ watchedScope: string[],
43
+ ig: ReturnType<typeof ignore>,
44
+ depth = 0,
45
+ maxDepth = 4
46
+ ): TreeNode[] {
47
+ if (depth > maxDepth) return [];
48
+
49
+ let entries: fs.Dirent[];
50
+ try {
51
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
52
+ } catch {
53
+ return [];
54
+ }
55
+
56
+ const nodes: TreeNode[] = [];
57
+
58
+ for (const entry of entries) {
59
+ const fullPath = path.join(dirPath, entry.name);
60
+ const relativePath = path.relative(projectRoot, fullPath);
61
+
62
+ if (IGNORE_DIRS.has(entry.name)) continue;
63
+ if (ig.ignores(relativePath)) continue;
64
+ if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
65
+
66
+ const inScope = activeScope.some(
67
+ (s) => relativePath.startsWith(s) || s.startsWith(relativePath)
68
+ );
69
+ const watched = watchedScope.some(
70
+ (s) => relativePath.startsWith(s) || relativePath === s
71
+ );
72
+
73
+ const node: TreeNode = {
74
+ name: entry.name,
75
+ isDir: entry.isDirectory(),
76
+ inScope,
77
+ watched,
78
+ relativePath,
79
+ modifiedAt: entry.isFile() ? getModifiedTime(fullPath) : undefined,
80
+ };
81
+
82
+ if (entry.isDirectory()) {
83
+ node.children = buildTree(
84
+ fullPath, projectRoot, activeScope, watchedScope, ig, depth + 1, maxDepth
85
+ );
86
+ }
87
+
88
+ nodes.push(node);
89
+ }
90
+
91
+ return nodes.sort((a, b) => {
92
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
93
+ return a.name.localeCompare(b.name);
94
+ });
95
+ }
96
+
97
+ function formatTimeAgo(date: Date): string {
98
+ const diff = Date.now() - date.getTime();
99
+ const mins = Math.floor(diff / 60000);
100
+ const hours = Math.floor(diff / 3600000);
101
+ const days = Math.floor(diff / 86400000);
102
+ if (mins < 60) return `${mins}m ago`;
103
+ if (hours < 24) return `${hours}h ago`;
104
+ return `${days}d ago`;
105
+ }
106
+
107
+ function renderTree(nodes: TreeNode[], prefix = "", isRoot = false): string {
108
+ const lines: string[] = [];
109
+
110
+ for (let i = 0; i < nodes.length; i++) {
111
+ const node = nodes[i];
112
+ const isLast = i === nodes.length - 1;
113
+ const connector = isLast ? "└── " : "├── ";
114
+ const childPrefix = prefix + (isLast ? " " : "│ ");
115
+
116
+ let annotation = "";
117
+ if (node.inScope) annotation += " ← ACTIVE SCOPE";
118
+ else if (node.watched) annotation += " ← WATCHED";
119
+
120
+ let timeAnnotation = "";
121
+ if (node.modifiedAt && !node.isDir) {
122
+ timeAnnotation = ` (${formatTimeAgo(node.modifiedAt)})`;
123
+ }
124
+
125
+ if (node.isDir && !node.inScope && !node.watched && !isRoot) {
126
+ const childCount = node.children?.length ?? 0;
127
+ lines.push(`${prefix}${connector}${node.name}/${annotation || ` ← out of scope (${childCount} items)`}`);
128
+ } else {
129
+ lines.push(`${prefix}${connector}${node.name}${node.isDir ? "/" : ""}${annotation}${timeAnnotation}`);
130
+ if (node.children && node.children.length > 0 && (node.inScope || node.watched || isRoot)) {
131
+ lines.push(renderTree(node.children, childPrefix));
132
+ }
133
+ }
134
+ }
135
+
136
+ return lines.filter(Boolean).join("\n");
137
+ }
138
+
139
+ export function generateFileTree(
140
+ projectRoot: string,
141
+ activeScope: string[],
142
+ watchedScope: string[]
143
+ ): string {
144
+ const ig = loadGitignore(projectRoot);
145
+ const projectName = path.basename(projectRoot);
146
+ const nodes = buildTree(projectRoot, projectRoot, activeScope, watchedScope, ig, 0);
147
+ const tree = renderTree(nodes, "", true);
148
+ return `${projectName}/\n${tree}`;
149
+ }
@@ -0,0 +1,74 @@
1
+ import simpleGit from "simple-git";
2
+
3
+ export interface GitContext {
4
+ branch: string;
5
+ lastCommit: {
6
+ hash: string;
7
+ message: string;
8
+ author: string;
9
+ date: string;
10
+ timeAgo: string;
11
+ } | null;
12
+ recentCommits: Array<{
13
+ hash: string;
14
+ message: string;
15
+ date: string;
16
+ }>;
17
+ uncommittedChanges: string[];
18
+ isGitRepo: boolean;
19
+ }
20
+
21
+ function timeAgo(dateStr: string): string {
22
+ const diff = Date.now() - new Date(dateStr).getTime();
23
+ const mins = Math.floor(diff / 60000);
24
+ const hours = Math.floor(diff / 3600000);
25
+ const days = Math.floor(diff / 86400000);
26
+ if (mins < 60) return `${mins} minutes ago`;
27
+ if (hours < 24) return `${hours} hours ago`;
28
+ return `${days} days ago`;
29
+ }
30
+
31
+ export async function getGitContext(projectRoot: string): Promise<GitContext> {
32
+ const git = simpleGit(projectRoot);
33
+
34
+ try {
35
+ const isRepo = await git.checkIsRepo();
36
+ if (!isRepo) {
37
+ return { branch: "", lastCommit: null, recentCommits: [], uncommittedChanges: [], isGitRepo: false };
38
+ }
39
+
40
+ const [branch, log, status] = await Promise.all([
41
+ git.revparse(["--abbrev-ref", "HEAD"]),
42
+ git.log({ maxCount: 5 }),
43
+ git.status(),
44
+ ]);
45
+
46
+ const [latest, ...rest] = log.all;
47
+
48
+ return {
49
+ isGitRepo: true,
50
+ branch: branch.trim(),
51
+ lastCommit: latest
52
+ ? {
53
+ hash: latest.hash.slice(0, 7),
54
+ message: latest.message,
55
+ author: latest.author_name,
56
+ date: latest.date,
57
+ timeAgo: timeAgo(latest.date),
58
+ }
59
+ : null,
60
+ recentCommits: rest.map((c) => ({
61
+ hash: c.hash.slice(0, 7),
62
+ message: c.message,
63
+ date: c.date,
64
+ })),
65
+ uncommittedChanges: [
66
+ ...status.modified,
67
+ ...status.created,
68
+ ...status.deleted,
69
+ ],
70
+ };
71
+ } catch {
72
+ return { branch: "", lastCommit: null, recentCommits: [], uncommittedChanges: [], isGitRepo: false };
73
+ }
74
+ }