ctxora 6.2.1 → 6.2.2

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
@@ -67,6 +67,7 @@ Codex · Claude Code · Cursor · Copilot · MCP clients
67
67
  ### Local context engine
68
68
 
69
69
  - AST-aware chunking for Python, JavaScript, and TypeScript, with bounded fallback chunking for other text formats.
70
+ - Framework-agnostic setup, indexing, retrieval, and MCP startup verified for Python, TypeScript, Flutter, Go, Rust, Java, Kotlin, Swift, PHP, and Ruby repositories.
70
71
  - Hybrid lexical and local semantic retrieval using BM25, TF-IDF/LSA, keyword overlap, symbols, and paths.
71
72
  - Code dependency graph traversal and graph-augmented context selection.
72
73
  - CAG, RAG, hybrid CAG/RAG, long-context, and graph-augmented planning strategies.
package/README.vi.md CHANGED
@@ -67,6 +67,7 @@ Codex · Claude Code · Cursor · Copilot · MCP clients
67
67
  ### Local context engine
68
68
 
69
69
  - AST-aware chunking cho Python, JavaScript và TypeScript; fallback chunking có giới hạn cho định dạng text khác.
70
+ - Luồng setup, indexing, retrieval và MCP startup độc lập framework, được kiểm thử với Python, TypeScript, Flutter, Go, Rust, Java, Kotlin, Swift, PHP và Ruby.
70
71
  - Hybrid lexical và local semantic retrieval bằng BM25, TF-IDF/LSA, keyword overlap, symbol và path.
71
72
  - Code dependency graph và graph-augmented context selection.
72
73
  - Các strategy CAG, RAG, hybrid CAG/RAG, long context và graph augmented.
package/bin/ctxora.mjs CHANGED
@@ -6,7 +6,7 @@ import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { spawnSync } from "node:child_process";
8
8
 
9
- const PACKAGE_VERSION = "6.2.1";
9
+ const PACKAGE_VERSION = "6.2.2";
10
10
  const PYTHON_RANGE = "3.10-3.13";
11
11
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxora",
3
- "version": "6.2.1",
3
+ "version": "6.2.2",
4
4
  "description": "Local-first context engine for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ctxora-engine"
7
- version = "6.2.1"
7
+ version = "6.2.2"
8
8
  description = "Local-first context engine for coding agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import fnmatch
4
+ import os
4
5
  from pathlib import Path
5
6
 
6
7
  from harness_context.schemas import HarnessError
@@ -9,8 +10,11 @@ from harness_context.workspace.policy import WorkspacePolicy
9
10
 
10
11
  DEFAULT_DENY = (
11
12
  ".git", ".hg", ".svn", ".ctxora", ".harness", ".ecc", "node_modules", "vendor", ".venv", "venv",
12
- "dist", "build", "__pycache__", ".env", ".env.*", "*.pem", "*.key", "id_rsa", "id_ed25519",
13
- "*.sqlite", "*.sqlite3", "*.db", "*.egg-info",
13
+ "dist", "build", "out", "target", "coverage", ".dart_tool", ".gradle", "Pods", "DerivedData",
14
+ ".build", "obj", ".next", ".nuxt", ".svelte-kit", ".angular", ".turbo", ".parcel-cache",
15
+ ".expo", ".wrangler", ".serverless", ".terraform", "oh_modules", ".cache", ".pytest_cache",
16
+ ".mypy_cache", ".ruff_cache", ".tox", ".nox", "__pycache__", ".env", ".env.*",
17
+ "*.pem", "*.key", "id_rsa", "id_ed25519", "*.sqlite", "*.sqlite3", "*.db", "*.egg-info",
14
18
  )
15
19
 
16
20
 
@@ -53,9 +57,37 @@ class WorkspaceRegistry:
53
57
  for name in (".gitignore", ".ctxoraignore", ".harnessignore"):
54
58
  ignore = root / name
55
59
  if ignore.is_file():
56
- patterns.extend(line.strip().lstrip("/") for line in ignore.read_text("utf-8").splitlines() if line.strip() and not line.lstrip().startswith("#") and not line.startswith("!"))
60
+ for line in ignore.read_text("utf-8").splitlines():
61
+ pattern = line.strip()
62
+ if not pattern or pattern.startswith("#") or pattern.startswith("!"):
63
+ continue
64
+ patterns.append(pattern.lstrip("/").rstrip("/"))
57
65
  return patterns
58
66
 
67
+ @staticmethod
68
+ def _ignored(relative: str, path: Path, patterns: list[str]) -> bool:
69
+ parts = path.parts
70
+ for pattern in patterns:
71
+ if relative == pattern or relative.startswith(f"{pattern}/"):
72
+ return True
73
+ if fnmatch.fnmatch(relative, pattern) or fnmatch.fnmatch(relative, f"{pattern}/*"):
74
+ return True
75
+ if any(fnmatch.fnmatch(part, pattern) for part in parts):
76
+ return True
77
+ return False
78
+
79
+ @classmethod
80
+ def _candidate_files(cls, root: Path, patterns: list[str]):
81
+ for current, directories, files in os.walk(root, topdown=True, followlinks=False):
82
+ current_path = Path(current)
83
+ directories[:] = [
84
+ name for name in directories
85
+ if not (current_path / name).is_symlink()
86
+ and not cls._ignored((current_path / name).relative_to(root).as_posix(), current_path / name, patterns)
87
+ ]
88
+ for name in files:
89
+ yield current_path / name
90
+
59
91
  def files(self, workspace_id: str, paths: list[str]) -> list[Path]:
60
92
  policy = self.get(workspace_id)
61
93
  selected, total = [], 0
@@ -64,12 +96,12 @@ class WorkspaceRegistry:
64
96
  continue
65
97
  base = source if source.is_dir() else source.parent
66
98
  patterns = self._patterns(base)
67
- candidates = [source] if source.is_file() else source.rglob("*")
99
+ candidates = [source] if source.is_file() else self._candidate_files(source, patterns)
68
100
  for path in candidates:
69
101
  if not path.is_file() or path.is_symlink():
70
102
  continue
71
103
  relative = path.relative_to(base).as_posix()
72
- if any(fnmatch.fnmatch(relative, pattern) or any(fnmatch.fnmatch(part, pattern) for part in path.parts) for pattern in patterns):
104
+ if self._ignored(relative, path, patterns):
73
105
  continue
74
106
  size = path.stat().st_size
75
107
  if size > policy.max_file_bytes:
@@ -77,6 +109,10 @@ class WorkspaceRegistry:
77
109
  content = path.read_bytes()
78
110
  if b"\x00" in content[:4096] or contains_secret(content):
79
111
  continue
112
+ try:
113
+ content.decode("utf-8")
114
+ except UnicodeDecodeError:
115
+ continue
80
116
  total += size
81
117
  if total > policy.max_total_bytes or len(selected) >= policy.max_files:
82
118
  raise HarnessError("workspace_limit", "workspace indexing limits exceeded")