@kentwynn/kgraph 0.1.4 → 0.1.6

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 CHANGED
@@ -86,10 +86,10 @@ KGraph writes local instruction files and command/prompt packs so AI tools can u
86
86
 
87
87
  | Integration | Always-on guidance | KGraph command assets |
88
88
  | --- | --- | --- |
89
- | Codex | `AGENTS.md` | `.agents/skills/kgraph*/SKILL.md` |
90
- | GitHub Copilot | `.github/copilot-instructions.md` | `.github/prompts/kgraph*.prompt.md` |
91
- | Cursor | `.cursor/rules/kgraph.mdc` | `.cursor/rules/kgraph-commands.mdc` |
92
- | Claude Code | `CLAUDE.md` | `.claude/commands/kgraph*.md` |
89
+ | Codex | `AGENTS.md` | `.agents/skills/kgraph/SKILL.md` |
90
+ | GitHub Copilot | `.github/copilot-instructions.md` | `.github/prompts/kgraph.prompt.md` |
91
+ | Cursor | `.cursor/rules/kgraph.mdc` | Built into the KGraph Cursor rule |
92
+ | Claude Code | `CLAUDE.md` | `.claude/commands/kgraph.md` |
93
93
 
94
94
  Example:
95
95
 
@@ -98,13 +98,13 @@ kgraph integrate add codex copilot cursor claude-code
98
98
  kgraph integrate list
99
99
  ```
100
100
 
101
- This gives supported tools reusable KGraph workflows similar to Spec Kit-style commands:
101
+ This gives each supported tool one reusable KGraph entry point similar to a Spec Kit-style command:
102
102
 
103
103
  - KGraph context: query `kgraph context "<topic>"` before broad repo exploration
104
104
  - KGraph update: save durable chat/debugging/workflow discoveries to `.kgraph/inbox/`, then run `kgraph update`
105
105
  - KGraph scan: run `kgraph scan` after refactors, file moves, renamed functions, or dependency changes
106
106
 
107
- The exact invocation depends on the host tool. Copilot uses prompt files, Codex uses skills, Cursor uses rules, and Claude Code uses command files.
107
+ The exact invocation depends on the host tool. Copilot uses one prompt file, Codex uses one skill, Cursor uses one rule, and Claude Code uses one command file. Scan and update are workflows inside that single KGraph entry point, not separate duplicated commands.
108
108
 
109
109
  KGraph-managed instruction blocks preserve existing user-authored content.
110
110
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
2
+ import { Command } from 'commander';
3
3
  export declare function createProgram(): Command;
package/dist/cli/index.js CHANGED
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { Command } from "commander";
5
- import { registerInitCommand } from "./commands/init.js";
6
- import { registerScanCommand } from "./commands/scan.js";
7
- import { registerUpdateCommand } from "./commands/update.js";
8
- import { registerContextCommand } from "./commands/context.js";
9
- import { registerIntegrateCommand } from "./commands/integrate.js";
10
- import { renderRootHelp } from "./help.js";
2
+ import { Command } from 'commander';
3
+ import { realpathSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { registerContextCommand } from './commands/context.js';
7
+ import { registerInitCommand } from './commands/init.js';
8
+ import { registerIntegrateCommand } from './commands/integrate.js';
9
+ import { registerScanCommand } from './commands/scan.js';
10
+ import { registerUpdateCommand } from './commands/update.js';
11
+ import { renderRootHelp } from './help.js';
12
+ const require = createRequire(import.meta.url);
13
+ const { version } = require('../../package.json');
11
14
  export function createProgram() {
12
15
  const program = new Command();
13
16
  program
14
- .name("kgraph")
15
- .description("Persistent repo intelligence for AI coding assistants")
16
- .version("0.1.2")
17
- .addHelpText("beforeAll", renderRootHelp())
17
+ .name('kgraph')
18
+ .description('Persistent repo intelligence for AI coding assistants')
19
+ .version(version)
20
+ .addHelpText('beforeAll', renderRootHelp())
18
21
  .helpOption(false);
19
- program.option("-h, --help", "Show this help");
20
- program.hook("preAction", (thisCommand) => {
22
+ program.option('-h, --help', 'Show this help');
23
+ program.hook('preAction', (thisCommand) => {
21
24
  if (thisCommand.opts().help) {
22
25
  console.log(renderRootHelp());
23
26
  process.exitCode = 0;
@@ -32,7 +35,9 @@ export function createProgram() {
32
35
  }
33
36
  if (isCliEntrypoint()) {
34
37
  const program = createProgram();
35
- if (process.argv.length <= 2 || process.argv.includes("-h") || process.argv.includes("--help")) {
38
+ if (process.argv.length <= 2 ||
39
+ process.argv.includes('-h') ||
40
+ process.argv.includes('--help')) {
36
41
  console.log(renderRootHelp());
37
42
  }
38
43
  else {
@@ -44,7 +49,8 @@ function isCliEntrypoint() {
44
49
  return false;
45
50
  }
46
51
  try {
47
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
52
+ return (realpathSync(fileURLToPath(import.meta.url)) ===
53
+ realpathSync(process.argv[1]));
48
54
  }
49
55
  catch {
50
56
  return import.meta.url === `file://${process.argv[1]}`;
@@ -1,4 +1,4 @@
1
- import type { KGraphConfig, KGraphWorkspace } from "../types/config.js";
1
+ import type { KGraphConfig, KGraphWorkspace } from '../types/config.js';
2
2
  export declare const DEFAULT_CONFIG: KGraphConfig;
3
3
  export declare function writeDefaultConfig(workspace: KGraphWorkspace): Promise<boolean>;
4
4
  export declare function saveConfig(workspace: KGraphWorkspace, config: KGraphConfig): Promise<void>;
@@ -1,62 +1,62 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import YAML from "yaml";
3
- import { pathExists } from "../storage/kgraph-paths.js";
4
- import { KGraphError } from "../cli/errors.js";
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import YAML from 'yaml';
3
+ import { KGraphError } from '../cli/errors.js';
4
+ import { pathExists } from '../storage/kgraph-paths.js';
5
5
  export const DEFAULT_CONFIG = {
6
- include: ["**/*"],
6
+ include: ['**/*'],
7
7
  exclude: [
8
- ".git",
9
- "node_modules",
10
- "dist",
11
- "build",
12
- ".next",
13
- "coverage",
14
- ".kgraph",
15
- ".npm-cache",
16
- ".cache",
17
- ".turbo",
18
- ".vite",
19
- ".nuxt",
20
- ".output",
21
- ".vercel",
22
- ".serverless",
23
- ".agents",
24
- ".specify",
25
- "specs",
26
- ".cursor",
27
- ".claude",
28
- ".github/copilot-instructions.md",
29
- ".github/prompts",
30
- "AGENTS.md",
31
- "CLAUDE.md",
32
- "REQUIREMENTS.md",
33
- "*.log",
34
- "*.tgz",
35
- ".DS_Store"
8
+ '.git',
9
+ 'node_modules',
10
+ 'dist',
11
+ 'build',
12
+ '.next',
13
+ 'coverage',
14
+ '.kgraph',
15
+ '.npm-cache',
16
+ '.cache',
17
+ '.turbo',
18
+ '.vite',
19
+ '.nuxt',
20
+ '.output',
21
+ '.vercel',
22
+ '.serverless',
23
+ '.agents',
24
+ '.specify',
25
+ 'specs',
26
+ '.cursor',
27
+ '.claude',
28
+ '.github/copilot-instructions.md',
29
+ '.github/prompts',
30
+ 'AGENTS.md',
31
+ 'CLAUDE.md',
32
+ 'REQUIREMENTS.md',
33
+ '*.log',
34
+ '*.tgz',
35
+ '.DS_Store',
36
36
  ],
37
37
  languages: {
38
- precise: [".js", ".jsx", ".ts", ".tsx"]
38
+ precise: ['.js', '.jsx', '.ts', '.tsx'],
39
39
  },
40
40
  maxContextItems: 8,
41
41
  domainHints: {},
42
- integrations: []
42
+ integrations: [],
43
43
  };
44
44
  export async function writeDefaultConfig(workspace) {
45
45
  if (await pathExists(workspace.configPath)) {
46
46
  return false;
47
47
  }
48
- await writeFile(workspace.configPath, YAML.stringify(DEFAULT_CONFIG), "utf8");
48
+ await writeFile(workspace.configPath, YAML.stringify(DEFAULT_CONFIG), 'utf8');
49
49
  return true;
50
50
  }
51
51
  export async function saveConfig(workspace, config) {
52
- await writeFile(workspace.configPath, YAML.stringify(config), "utf8");
52
+ await writeFile(workspace.configPath, YAML.stringify(config), 'utf8');
53
53
  }
54
54
  export async function loadConfig(workspace) {
55
55
  if (!(await pathExists(workspace.configPath))) {
56
56
  return DEFAULT_CONFIG;
57
57
  }
58
58
  try {
59
- const raw = await readFile(workspace.configPath, "utf8");
59
+ const raw = await readFile(workspace.configPath, 'utf8');
60
60
  const parsed = YAML.parse(raw);
61
61
  return normalizeConfig(parsed ?? {});
62
62
  }
@@ -67,18 +67,22 @@ export async function loadConfig(workspace) {
67
67
  }
68
68
  export function normalizeConfig(config) {
69
69
  return {
70
- include: Array.isArray(config.include) ? config.include : DEFAULT_CONFIG.include,
70
+ include: Array.isArray(config.include)
71
+ ? config.include
72
+ : DEFAULT_CONFIG.include,
71
73
  exclude: mergeUnique(DEFAULT_CONFIG.exclude, Array.isArray(config.exclude) ? config.exclude : []),
72
74
  languages: {
73
75
  precise: Array.isArray(config.languages?.precise)
74
76
  ? config.languages.precise
75
- : DEFAULT_CONFIG.languages.precise
77
+ : DEFAULT_CONFIG.languages.precise,
76
78
  },
77
- maxContextItems: typeof config.maxContextItems === "number" && config.maxContextItems > 0
79
+ maxContextItems: typeof config.maxContextItems === 'number' && config.maxContextItems > 0
78
80
  ? config.maxContextItems
79
81
  : DEFAULT_CONFIG.maxContextItems,
80
- domainHints: config.domainHints && typeof config.domainHints === "object" ? config.domainHints : {},
81
- integrations: normalizeIntegrations(config.integrations)
82
+ domainHints: config.domainHints && typeof config.domainHints === 'object'
83
+ ? config.domainHints
84
+ : {},
85
+ integrations: normalizeIntegrations(config.integrations),
82
86
  };
83
87
  }
84
88
  function mergeUnique(base, extra) {
@@ -91,23 +95,23 @@ function normalizeIntegrations(value) {
91
95
  const seen = new Set();
92
96
  const integrations = [];
93
97
  for (const item of value) {
94
- if (!item || typeof item !== "object") {
98
+ if (!item || typeof item !== 'object') {
95
99
  continue;
96
100
  }
97
101
  const candidate = item;
98
- if (typeof candidate.name !== "string" ||
99
- typeof candidate.targetPath !== "string" ||
102
+ if (typeof candidate.name !== 'string' ||
103
+ typeof candidate.targetPath !== 'string' ||
100
104
  seen.has(candidate.name)) {
101
105
  continue;
102
106
  }
103
- if (!["claude-code", "codex", "copilot", "cursor"].includes(candidate.name)) {
107
+ if (!['claude-code', 'codex', 'copilot', 'cursor'].includes(candidate.name)) {
104
108
  continue;
105
109
  }
106
110
  seen.add(candidate.name);
107
111
  integrations.push({
108
112
  name: candidate.name,
109
113
  enabled: candidate.enabled !== false,
110
- targetPath: candidate.targetPath
114
+ targetPath: candidate.targetPath,
111
115
  });
112
116
  }
113
117
  return integrations;
@@ -1,2 +1,2 @@
1
- import type { IntegrationAdapter } from "../integration-registry.js";
1
+ import type { IntegrationAdapter } from '../integration-registry.js';
2
2
  export declare const claudeCodeAdapter: IntegrationAdapter;
@@ -1,7 +1,7 @@
1
1
  export const claudeCodeAdapter = {
2
- name: "claude-code",
3
- label: "Claude Code",
4
- targetPath: "CLAUDE.md",
2
+ name: 'claude-code',
3
+ label: 'Claude Code',
4
+ targetPath: 'CLAUDE.md',
5
5
  instructions: `## KGraph Workflow
6
6
 
7
7
  - Start repository work by checking \`kgraph context "<topic>"\` when the user asks about a domain, bug, workflow, or feature.
@@ -12,28 +12,26 @@ export const claudeCodeAdapter = {
12
12
  `,
13
13
  commandFiles: [
14
14
  {
15
- path: ".claude/commands/kgraph.md",
15
+ path: '.claude/commands/kgraph.md',
16
16
  content: `Use KGraph persistent repo intelligence for the current request.
17
17
 
18
18
  1. Infer the topic from the user's request.
19
19
  2. Run \`kgraph context "<topic>"\`.
20
20
  3. Use the returned files, symbols, relationships, and cognition before broad exploration.
21
21
  4. Save durable discoveries to \`.kgraph/inbox/\` and run \`kgraph update\` when appropriate.
22
- `
22
+ 5. Run \`kgraph scan\` after structural changes and report the scan summary.
23
+ `,
23
24
  },
24
25
  {
25
- path: ".claude/commands/kgraph-update.md",
26
- content: `Preserve useful chat knowledge in KGraph.
27
-
28
- Create a concise Markdown note in \`.kgraph/inbox/\` from durable discoveries in this conversation, then run \`kgraph update\`.
29
- `
26
+ path: '.claude/commands/kgraph-scan.md',
27
+ content: `Run \`kgraph scan\` to refresh the repository maps, then summarize what changed.
28
+ `,
30
29
  },
31
30
  {
32
- path: ".claude/commands/kgraph-scan.md",
33
- content: `Refresh KGraph structural maps.
34
-
35
- Run \`kgraph scan\` after code structure changes and report the scan summary.
36
- `
37
- }
38
- ]
31
+ path: '.claude/commands/kgraph-update.md',
32
+ content: `Run \`kgraph update\` to process any pending Markdown notes in \`.kgraph/inbox/\` into durable cognition.
33
+ `,
34
+ },
35
+ ],
36
+ obsoleteCommandFiles: [],
39
37
  };
@@ -28,29 +28,8 @@ Workflow:
28
28
  4. After durable discoveries, write a concise Markdown note to \`.kgraph/inbox/\`.
29
29
  5. Run \`kgraph update\` if you created an inbox note.
30
30
  6. Run \`kgraph scan\` after structural changes.
31
- `
32
- },
33
- {
34
- path: ".agents/skills/kgraph-update/SKILL.md",
35
- content: `---
36
- name: "kgraph-update"
37
- description: "Preserve useful chat discoveries into KGraph cognition."
38
- ---
39
-
40
- Use this skill when the user asks to update KGraph memory or preserve what was learned.
41
-
42
- Write a concise Markdown note under \`.kgraph/inbox/\` with durable architecture, debugging, workflow, file, symbol, or gotcha knowledge. Then run \`kgraph update\`.
43
- `
44
- },
45
- {
46
- path: ".agents/skills/kgraph-scan/SKILL.md",
47
- content: `---
48
- name: "kgraph-scan"
49
- description: "Refresh KGraph structural maps after code changes."
50
- ---
51
-
52
- Run \`kgraph scan\` after files move, functions are renamed, folders are refactored, or dependencies change. Report the scan summary and any obvious exclude/config problems.
53
31
  `
54
32
  }
55
- ]
33
+ ],
34
+ obsoleteCommandFiles: [".agents/skills/kgraph-update", ".agents/skills/kgraph-scan"]
56
35
  };
@@ -1,2 +1,2 @@
1
- import type { IntegrationAdapter } from "../integration-registry.js";
1
+ import type { IntegrationAdapter } from '../integration-registry.js';
2
2
  export declare const copilotAdapter: IntegrationAdapter;
@@ -1,7 +1,7 @@
1
1
  export const copilotAdapter = {
2
- name: "copilot",
3
- label: "GitHub Copilot",
4
- targetPath: ".github/copilot-instructions.md",
2
+ name: 'copilot',
3
+ label: 'GitHub Copilot',
4
+ targetPath: '.github/copilot-instructions.md',
5
5
  instructions: `## KGraph Workflow
6
6
 
7
7
  - Use \`kgraph context "<topic>"\` before scanning many files manually.
@@ -12,7 +12,7 @@ export const copilotAdapter = {
12
12
  `,
13
13
  commandFiles: [
14
14
  {
15
- path: ".github/prompts/kgraph.prompt.md",
15
+ path: '.github/prompts/kgraph.prompt.md',
16
16
  content: `---
17
17
  mode: agent
18
18
  description: Use KGraph persistent repo intelligence for this request
@@ -25,29 +25,28 @@ Use KGraph for the current task.
25
25
  3. If you discover durable architecture, debugging, workflow, or gotcha knowledge, create a Markdown note in \`.kgraph/inbox/\`.
26
26
  4. If you add an inbox note, run \`kgraph update\`.
27
27
  5. If code structure changed, run \`kgraph scan\`.
28
- `
28
+ `,
29
29
  },
30
30
  {
31
- path: ".github/prompts/kgraph-update.prompt.md",
31
+ path: '.github/prompts/kgraph-scan.prompt.md',
32
32
  content: `---
33
33
  mode: agent
34
- description: Preserve recent useful chat knowledge in KGraph
34
+ description: Refresh KGraph file, symbol, import, and relationship maps
35
35
  ---
36
36
 
37
- Create a concise Markdown cognition note in \`.kgraph/inbox/\` from the useful stable knowledge in this conversation, then run \`kgraph update\`.
38
-
39
- Capture only durable information: architecture discoveries, debugging conclusions, workflow conventions, important files, important functions, and gotchas. Do not store temporary chatter.
40
- `
37
+ Run \`kgraph scan\` to refresh the repository maps, then summarize what changed.
38
+ `,
41
39
  },
42
40
  {
43
- path: ".github/prompts/kgraph-scan.prompt.md",
41
+ path: '.github/prompts/kgraph-update.prompt.md',
44
42
  content: `---
45
43
  mode: agent
46
- description: Refresh KGraph structural maps after repository changes
44
+ description: Process KGraph inbox notes into durable cognition
47
45
  ---
48
46
 
49
- Run \`kgraph scan\` and report the number of files and symbols scanned. If scan output suggests generated/cache files were included, mention that the exclude configuration may need adjustment.
50
- `
51
- }
52
- ]
47
+ Run \`kgraph update\` to process any pending Markdown notes in \`.kgraph/inbox/\` into durable cognition.
48
+ `,
49
+ },
50
+ ],
51
+ obsoleteCommandFiles: [],
53
52
  };
@@ -15,22 +15,5 @@ alwaysApply: true
15
15
  - Run \`kgraph scan\` after refactors, moved folders, renamed functions, or other structure changes.
16
16
  - Run \`kgraph visualize\` when visualization support is available and the developer asks to inspect the KGraph map.
17
17
  `,
18
- commandFiles: [
19
- {
20
- path: ".cursor/rules/kgraph-commands.mdc",
21
- content: `---
22
- description: KGraph command workflows for repo context, update, and scan
23
- alwaysApply: false
24
- ---
25
-
26
- # KGraph Commands
27
-
28
- Use these workflows when the user asks for KGraph:
29
-
30
- - KGraph context: run \`kgraph context "<topic>"\` before broad exploration.
31
- - KGraph update: write durable findings to \`.kgraph/inbox/\`, then run \`kgraph update\`.
32
- - KGraph scan: run \`kgraph scan\` after refactors, file moves, renamed functions, or dependency changes.
33
- `
34
- }
35
- ]
18
+ obsoleteCommandFiles: [".cursor/rules/kgraph-commands.mdc"]
36
19
  };
@@ -5,6 +5,7 @@ export interface IntegrationAdapter {
5
5
  targetPath: string;
6
6
  instructions: string;
7
7
  commandFiles?: IntegrationCommandFile[];
8
+ obsoleteCommandFiles?: string[];
8
9
  }
9
10
  export interface IntegrationCommandFile {
10
11
  path: string;
@@ -27,6 +27,7 @@ export async function addIntegrations(workspace, names) {
27
27
  };
28
28
  byName.set(adapter.name, next);
29
29
  await writeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name, adapter.instructions);
30
+ await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
30
31
  await writeIntegrationCommandFiles(workspace.rootPath, adapter.commandFiles ?? []);
31
32
  changed.push(next);
32
33
  }
@@ -42,6 +43,7 @@ export async function removeIntegrations(workspace, names) {
42
43
  const adapter = getIntegrationAdapter(name);
43
44
  await removeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name);
44
45
  await removeIntegrationCommandFiles(workspace.rootPath, adapter.commandFiles ?? []);
46
+ await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
45
47
  removed.push(adapter.name);
46
48
  }
47
49
  config.integrations = config.integrations.filter((integration) => !removeNames.has(integration.name));
@@ -77,6 +79,7 @@ async function writeIntegrationCommandFiles(rootPath, files) {
77
79
  }
78
80
  async function removeIntegrationCommandFiles(rootPath, files) {
79
81
  for (const file of files) {
80
- await rm(path.join(rootPath, file.path), { force: true });
82
+ const filePath = typeof file === "string" ? file : file.path;
83
+ await rm(path.join(rootPath, filePath), { force: true, recursive: true });
81
84
  }
82
85
  }
@@ -1,5 +1,6 @@
1
- import type { KGraphConfig } from "../types/config.js";
1
+ import type { KGraphConfig } from '../types/config.js';
2
2
  export declare function shouldExclude(repoPath: string, config: KGraphConfig): boolean;
3
3
  export declare function buildFastGlobIgnore(exclude: string[]): string[];
4
+ export declare function readGitignorePatterns(rootPath: string): Promise<string[]>;
4
5
  export declare function detectLanguage(filePath: string): string;
5
6
  export declare function isPreciseLanguage(filePath: string, config: KGraphConfig): boolean;
@@ -1,13 +1,88 @@
1
- import path from "node:path";
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  const LANGUAGE_BY_EXTENSION = {
3
- ".js": "javascript",
4
- ".jsx": "javascriptreact",
5
- ".ts": "typescript",
6
- ".tsx": "typescriptreact",
7
- ".json": "json",
8
- ".md": "markdown",
9
- ".yaml": "yaml",
10
- ".yml": "yaml"
4
+ // JavaScript / TypeScript
5
+ '.js': 'javascript',
6
+ '.jsx': 'javascriptreact',
7
+ '.ts': 'typescript',
8
+ '.tsx': 'typescriptreact',
9
+ '.mjs': 'javascript',
10
+ '.cjs': 'javascript',
11
+ '.mts': 'typescript',
12
+ '.cts': 'typescript',
13
+ // Python
14
+ '.py': 'python',
15
+ '.pyw': 'python',
16
+ '.pyi': 'python',
17
+ // Go
18
+ '.go': 'go',
19
+ // Rust
20
+ '.rs': 'rust',
21
+ // Java / JVM
22
+ '.java': 'java',
23
+ '.kt': 'kotlin',
24
+ '.kts': 'kotlin',
25
+ '.scala': 'scala',
26
+ '.groovy': 'groovy',
27
+ // C / C++
28
+ '.c': 'c',
29
+ '.h': 'c',
30
+ '.cpp': 'cpp',
31
+ '.cc': 'cpp',
32
+ '.cxx': 'cpp',
33
+ '.hpp': 'cpp',
34
+ '.hxx': 'cpp',
35
+ // C#
36
+ '.cs': 'csharp',
37
+ // Ruby
38
+ '.rb': 'ruby',
39
+ '.rake': 'ruby',
40
+ // PHP
41
+ '.php': 'php',
42
+ // Swift
43
+ '.swift': 'swift',
44
+ // Shell
45
+ '.sh': 'shell',
46
+ '.bash': 'shell',
47
+ '.zsh': 'shell',
48
+ '.fish': 'shell',
49
+ // Web
50
+ '.html': 'html',
51
+ '.htm': 'html',
52
+ '.css': 'css',
53
+ '.scss': 'scss',
54
+ '.sass': 'sass',
55
+ '.less': 'less',
56
+ '.vue': 'vue',
57
+ '.svelte': 'svelte',
58
+ // Data / Config
59
+ '.json': 'json',
60
+ '.jsonc': 'json',
61
+ '.yaml': 'yaml',
62
+ '.yml': 'yaml',
63
+ '.toml': 'toml',
64
+ '.xml': 'xml',
65
+ '.graphql': 'graphql',
66
+ '.gql': 'graphql',
67
+ // Docs
68
+ '.md': 'markdown',
69
+ '.mdx': 'markdown',
70
+ '.rst': 'restructuredtext',
71
+ '.tex': 'latex',
72
+ // Other
73
+ '.lua': 'lua',
74
+ '.r': 'r',
75
+ '.R': 'r',
76
+ '.dart': 'dart',
77
+ '.ex': 'elixir',
78
+ '.exs': 'elixir',
79
+ '.erl': 'erlang',
80
+ '.hrl': 'erlang',
81
+ '.hs': 'haskell',
82
+ '.clj': 'clojure',
83
+ '.tf': 'terraform',
84
+ '.proto': 'protobuf',
85
+ '.sql': 'sql',
11
86
  };
12
87
  export function shouldExclude(repoPath, config) {
13
88
  const normalizedPath = normalizeRepoPath(repoPath);
@@ -16,7 +91,7 @@ export function shouldExclude(repoPath, config) {
16
91
  export function buildFastGlobIgnore(exclude) {
17
92
  const patterns = new Set();
18
93
  for (const pattern of exclude) {
19
- const normalized = normalizeRepoPath(pattern).replace(/\/$/, "");
94
+ const normalized = normalizeRepoPath(pattern).replace(/\/$/, '');
20
95
  if (!normalized) {
21
96
  continue;
22
97
  }
@@ -32,50 +107,63 @@ export function buildFastGlobIgnore(exclude) {
32
107
  }
33
108
  return [...patterns];
34
109
  }
110
+ export async function readGitignorePatterns(rootPath) {
111
+ try {
112
+ const raw = await readFile(path.join(rootPath, '.gitignore'), 'utf8');
113
+ return raw
114
+ .split('\n')
115
+ .map((line) => line.trim())
116
+ .filter((line) => line.length > 0 && !line.startsWith('#') && !line.startsWith('!'));
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ }
35
122
  export function detectLanguage(filePath) {
36
- return LANGUAGE_BY_EXTENSION[path.extname(filePath)] ?? "unknown";
123
+ return LANGUAGE_BY_EXTENSION[path.extname(filePath)] ?? 'unknown';
37
124
  }
38
125
  export function isPreciseLanguage(filePath, config) {
39
126
  return config.languages.precise.includes(path.extname(filePath));
40
127
  }
41
128
  function matchesExcludePattern(repoPath, pattern) {
42
- const normalized = normalizeRepoPath(pattern).replace(/\/$/, "");
129
+ const normalized = normalizeRepoPath(pattern).replace(/\/$/, '');
43
130
  if (!normalized) {
44
131
  return false;
45
132
  }
46
133
  if (hasGlob(normalized)) {
47
- return globToRegExp(normalized).test(repoPath) || globToRegExp(`**/${normalized}`).test(repoPath);
134
+ return (globToRegExp(normalized).test(repoPath) ||
135
+ globToRegExp(`**/${normalized}`).test(repoPath));
48
136
  }
49
137
  if (repoPath === normalized || repoPath.startsWith(`${normalized}/`)) {
50
138
  return true;
51
139
  }
52
- if (!normalized.includes("/")) {
53
- return repoPath.split("/").includes(normalized);
140
+ if (!normalized.includes('/')) {
141
+ return repoPath.split('/').includes(normalized);
54
142
  }
55
143
  return false;
56
144
  }
57
145
  function normalizeRepoPath(value) {
58
- return value.replace(/\\/g, "/").replace(/^\.\/+/, "");
146
+ return value.replace(/\\/g, '/').replace(/^\.\/+/, '');
59
147
  }
60
148
  function hasGlob(pattern) {
61
149
  return /[*?[\]{}]/.test(pattern);
62
150
  }
63
151
  function globToRegExp(pattern) {
64
- let source = "";
152
+ let source = '';
65
153
  for (let index = 0; index < pattern.length; index += 1) {
66
154
  const char = pattern[index];
67
155
  const next = pattern[index + 1];
68
- if (char === "*" && next === "*") {
69
- source += ".*";
156
+ if (char === '*' && next === '*') {
157
+ source += '.*';
70
158
  index += 1;
71
159
  continue;
72
160
  }
73
- if (char === "*") {
74
- source += "[^/]*";
161
+ if (char === '*') {
162
+ source += '[^/]*';
75
163
  continue;
76
164
  }
77
- if (char === "?") {
78
- source += "[^/]";
165
+ if (char === '?') {
166
+ source += '[^/]';
79
167
  continue;
80
168
  }
81
169
  source += escapeRegExp(char);
@@ -83,5 +171,5 @@ function globToRegExp(pattern) {
83
171
  return new RegExp(`^${source}$`);
84
172
  }
85
173
  function escapeRegExp(value) {
86
- return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
174
+ return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
87
175
  }
@@ -1,3 +1,3 @@
1
- import type { KGraphConfig } from "../types/config.js";
2
- import type { ScanResult } from "../types/maps.js";
1
+ import type { KGraphConfig } from '../types/config.js';
2
+ import type { ScanResult } from '../types/maps.js';
3
3
  export declare function scanRepository(rootPath: string, config: KGraphConfig, previous?: ScanResult): Promise<ScanResult>;
@@ -1,16 +1,19 @@
1
- import { readFile, stat } from "node:fs/promises";
2
- import crypto from "node:crypto";
3
- import path from "node:path";
4
- import fg from "fast-glob";
5
- import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, shouldExclude } from "./file-classifier.js";
6
- import { extractTsSymbols } from "./ts-symbol-extractor.js";
1
+ import fg from 'fast-glob';
2
+ import crypto from 'node:crypto';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
6
+ import { extractTsSymbols } from './ts-symbol-extractor.js';
7
7
  export async function scanRepository(rootPath, config, previous) {
8
+ const gitignorePatterns = await readGitignorePatterns(rootPath);
9
+ const allExcludes = [...config.exclude, ...gitignorePatterns];
10
+ const mergedConfig = { ...config, exclude: allExcludes };
8
11
  const entries = await fg(config.include, {
9
12
  cwd: rootPath,
10
13
  dot: true,
11
14
  onlyFiles: true,
12
15
  unique: true,
13
- ignore: buildFastGlobIgnore(config.exclude)
16
+ ignore: buildFastGlobIgnore(allExcludes),
14
17
  });
15
18
  const files = [];
16
19
  const symbols = [];
@@ -18,14 +21,20 @@ export async function scanRepository(rootPath, config, previous) {
18
21
  const relationships = [];
19
22
  const warnings = [];
20
23
  for (const repoPath of entries.sort()) {
21
- if (shouldExclude(repoPath, config)) {
24
+ if (shouldExclude(repoPath, mergedConfig)) {
22
25
  continue;
23
26
  }
24
27
  const absolutePath = path.join(rootPath, repoPath);
25
28
  try {
26
- const [info, content] = await Promise.all([stat(absolutePath), readFile(absolutePath)]);
27
- const text = content.toString("utf8");
28
- const contentHash = crypto.createHash("sha256").update(content).digest("hex");
29
+ const [info, content] = await Promise.all([
30
+ stat(absolutePath),
31
+ readFile(absolutePath),
32
+ ]);
33
+ const text = content.toString('utf8');
34
+ const contentHash = crypto
35
+ .createHash('sha256')
36
+ .update(content)
37
+ .digest('hex');
29
38
  const file = {
30
39
  id: repoPath,
31
40
  path: repoPath,
@@ -34,8 +43,8 @@ export async function scanRepository(rootPath, config, previous) {
34
43
  sizeBytes: info.size,
35
44
  modifiedAt: info.mtime.toISOString(),
36
45
  contentHash,
37
- scanStatus: isPreciseLanguage(repoPath, config) ? "mapped" : "generic",
38
- warnings: []
46
+ scanStatus: isPreciseLanguage(repoPath, config) ? 'mapped' : 'generic',
47
+ warnings: [],
39
48
  };
40
49
  if (isPreciseLanguage(repoPath, config)) {
41
50
  const extracted = extractTsSymbols(text, repoPath);
@@ -55,9 +64,9 @@ export async function scanRepository(rootPath, config, previous) {
55
64
  extension: path.extname(repoPath),
56
65
  language: detectLanguage(repoPath),
57
66
  sizeBytes: 0,
58
- contentHash: "",
59
- scanStatus: "failed",
60
- warnings: [message]
67
+ contentHash: '',
68
+ scanStatus: 'failed',
69
+ warnings: [message],
61
70
  });
62
71
  }
63
72
  }
@@ -66,18 +75,22 @@ export async function scanRepository(rootPath, config, previous) {
66
75
  }
67
76
  function detectMovedFiles(previousFiles, currentFiles) {
68
77
  const currentPaths = new Set(currentFiles.map((file) => file.path));
69
- const previousByHash = new Map(previousFiles.filter((file) => file.contentHash).map((file) => [file.contentHash, file]));
78
+ const previousByHash = new Map(previousFiles
79
+ .filter((file) => file.contentHash)
80
+ .map((file) => [file.contentHash, file]));
70
81
  const relationships = [];
71
82
  for (const file of currentFiles) {
72
83
  const previous = previousByHash.get(file.contentHash);
73
- if (previous && previous.path !== file.path && !currentPaths.has(previous.path)) {
84
+ if (previous &&
85
+ previous.path !== file.path &&
86
+ !currentPaths.has(previous.path)) {
74
87
  relationships.push({
75
- sourceType: "file",
88
+ sourceType: 'file',
76
89
  sourceId: file.path,
77
- targetType: "file",
90
+ targetType: 'file',
78
91
  targetId: previous.path,
79
- relationshipType: "moved-from",
80
- confidence: "high"
92
+ relationshipType: 'moved-from',
93
+ confidence: 'high',
81
94
  });
82
95
  }
83
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {