nogic 0.0.1__tar.gz → 0.1.0__tar.gz

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 (43) hide show
  1. {nogic-0.0.1 → nogic-0.1.0}/PKG-INFO +2 -2
  2. {nogic-0.0.1 → nogic-0.1.0}/pyproject.toml +2 -2
  3. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/__init__.py +1 -1
  4. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/init.py +4 -5
  5. nogic-0.1.0/src/nogic/commands/reindex.py +89 -0
  6. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/status.py +3 -65
  7. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/sync.py +13 -16
  8. nogic-0.1.0/src/nogic/commands/watch.py +116 -0
  9. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/watcher/sync.py +101 -287
  10. nogic-0.0.1/src/nogic/commands/reindex.py +0 -117
  11. nogic-0.0.1/src/nogic/commands/watch.py +0 -167
  12. {nogic-0.0.1 → nogic-0.1.0}/.claude/settings.local.json +0 -0
  13. {nogic-0.0.1 → nogic-0.1.0}/.gitignore +0 -0
  14. {nogic-0.0.1 → nogic-0.1.0}/.python-version +0 -0
  15. {nogic-0.0.1 → nogic-0.1.0}/DEVELOPMENT.md +0 -0
  16. {nogic-0.0.1 → nogic-0.1.0}/LICENSE +0 -0
  17. {nogic-0.0.1 → nogic-0.1.0}/README.md +0 -0
  18. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/api/__init__.py +0 -0
  19. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/api/client.py +0 -0
  20. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/__init__.py +0 -0
  21. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/login.py +0 -0
  22. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/projects.py +0 -0
  23. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/commands/telemetry_cmd.py +0 -0
  24. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/config.py +0 -0
  25. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/ignore.py +0 -0
  26. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/main.py +0 -0
  27. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/parsing/__init__.py +0 -0
  28. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/parsing/js_extractor.py +0 -0
  29. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/parsing/parser.py +0 -0
  30. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/parsing/python_extractor.py +0 -0
  31. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/parsing/types.py +0 -0
  32. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/storage/__init__.py +0 -0
  33. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/storage/relationships.py +0 -0
  34. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/storage/schema.py +0 -0
  35. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/storage/symbols.py +0 -0
  36. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/telemetry.py +0 -0
  37. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/ui.py +0 -0
  38. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/watcher/__init__.py +0 -0
  39. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/watcher/monitor.py +0 -0
  40. {nogic-0.0.1 → nogic-0.1.0}/src/nogic/watcher/storage.py +0 -0
  41. {nogic-0.0.1 → nogic-0.1.0}/tests/test_e2e.py +0 -0
  42. {nogic-0.0.1 → nogic-0.1.0}/tests/test_ignore.py +0 -0
  43. {nogic-0.0.1 → nogic-0.1.0}/tests/test_sync_batching.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nogic
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Code intelligence CLI for AI agents — index, search, and understand codebases via graph + vector embeddings.
5
5
  Project-URL: Homepage, https://nogic.dev
6
6
  Project-URL: Repository, https://github.com/nogic-dev/cli
7
7
  Project-URL: Documentation, https://docs.nogic.dev
8
- Author-email: Nogic <support@nogic.dev>
8
+ Author-email: Nogic <hello@nogic.dev>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: ai-agents,code-graph,code-intelligence,embeddings,mcp,tree-sitter
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "nogic"
3
- version = "0.0.1"
3
+ version = "0.1.0"
4
4
  description = "Code intelligence CLI for AI agents — index, search, and understand codebases via graph + vector embeddings."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  requires-python = ">=3.11"
8
8
  authors = [
9
- { name = "Nogic", email = "support@nogic.dev" },
9
+ { name = "Nogic", email = "hello@nogic.dev" },
10
10
  ]
11
11
  classifiers = [
12
12
  "Development Status :: 4 - Beta",
@@ -1,3 +1,3 @@
1
1
  """Nogic CLI package."""
2
2
 
3
- __version__ = "0.0.1"
3
+ __version__ = "0.1.0"
@@ -17,7 +17,6 @@ def init(
17
17
  project_id: Annotated[Optional[str], typer.Option("--project-id", "-p", help="Use existing project ID.")] = None,
18
18
  name: Annotated[Optional[str], typer.Option("--name", "-n", help="Project name.")] = None,
19
19
  link: Annotated[bool, typer.Option("--link", help="Re-link an existing project.")] = False,
20
- yes: Annotated[bool, typer.Option("--yes", "-y", help="Accept defaults, skip prompts.")] = False,
21
20
  ):
22
21
  """Initialize a Nogic project in a directory."""
23
22
  directory = directory.resolve()
@@ -58,7 +57,7 @@ def init(
58
57
  ui.info(f"Found existing project '{existing_project.name}'")
59
58
  ui.kv("Project ID", existing_project.id)
60
59
 
61
- if yes or typer.confirm("Use this project?", default=True):
60
+ if typer.confirm("Use this project?", default=True):
62
61
  config.project_id = existing_project.id
63
62
  config.project_name = existing_project.name
64
63
  config.directory_hash = dir_hash
@@ -68,7 +67,7 @@ def init(
68
67
  ui.console.print(" 1. Wipe graph data and reuse this project")
69
68
  ui.console.print(" 2. Abort")
70
69
 
71
- choice = "1" if yes else typer.prompt("Choose option", default="1")
70
+ choice = typer.prompt("Choose option", default="1")
72
71
 
73
72
  if choice == "1":
74
73
  with ui.status_spinner("Wiping graph data..."):
@@ -82,7 +81,7 @@ def init(
82
81
  ui.dim("Aborted.")
83
82
  raise typer.Exit(0)
84
83
  else:
85
- project_name = name or (directory.name if yes else typer.prompt("Project name", default=directory.name))
84
+ project_name = name or typer.prompt("Project name", default=directory.name)
86
85
 
87
86
  try:
88
87
  with ui.status_spinner("Creating project..."):
@@ -96,7 +95,7 @@ def init(
96
95
  if e.response.status_code == 409:
97
96
  data = e.response.json()
98
97
  ui.warn(f"Project already exists: {data.get('project_name')}")
99
- if yes or typer.confirm("Use this project?", default=True):
98
+ if typer.confirm("Use this project?", default=True):
100
99
  config.project_id = data.get("project_id")
101
100
  config.project_name = data.get("project_name")
102
101
  config.directory_hash = dir_hash
@@ -0,0 +1,89 @@
1
+ """Reindex command - wipe graph data and re-index from scratch."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import httpx
7
+ import typer
8
+
9
+ from nogic.config import Config, CONFIG_DIR, is_dev_mode, get_api_url
10
+ from nogic.ignore import build_ignore_matcher
11
+ from nogic.watcher import SyncService
12
+ from nogic.api import NogicClient
13
+ from nogic import ui
14
+
15
+
16
+ def reindex(
17
+ directory: Annotated[Path, typer.Argument(help="Path to the project directory.")] = Path("."),
18
+ ignore: Annotated[Optional[list[str]], typer.Option("--ignore", help="Patterns to ignore.")] = None,
19
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation.")] = False,
20
+ ):
21
+ """Wipe graph data and re-index the entire project."""
22
+ directory = directory.resolve()
23
+ nogic_dir = directory / CONFIG_DIR
24
+ ignore = ignore or []
25
+
26
+ if not nogic_dir.exists():
27
+ ui.error("Not a Nogic project.")
28
+ ui.dim("Run `nogic init` to initialize your project.")
29
+ raise typer.Exit(1)
30
+
31
+ config = Config.load(directory)
32
+
33
+ if not config.api_key:
34
+ ui.error("Not logged in.")
35
+ ui.dim("Run `nogic login` to authenticate.")
36
+ raise typer.Exit(1)
37
+
38
+ if not config.project_id:
39
+ ui.error("No project configured.")
40
+ ui.dim("Run `nogic init` to initialize your project.")
41
+ raise typer.Exit(1)
42
+
43
+ if is_dev_mode():
44
+ ui.dev_banner(get_api_url())
45
+
46
+ if not yes:
47
+ ui.banner("nogic reindex")
48
+ ui.kv("Project", config.project_name or f"{config.project_id[:8]}...")
49
+ ui.kv("Directory", str(directory))
50
+ ui.console.print()
51
+ ui.warn("This will delete all graph data and re-index from scratch.")
52
+ ui.console.print()
53
+ if not typer.confirm("Continue?", default=False):
54
+ ui.dim("Aborted.")
55
+ raise typer.Exit(0)
56
+
57
+ client = NogicClient(config)
58
+ nodes_deleted = 0
59
+
60
+ try:
61
+ with ui.status_spinner("Wiping graph data..."):
62
+ try:
63
+ result = client.wipe_project_graph(config.project_id)
64
+ nodes_deleted = result.nodes_deleted
65
+ except httpx.HTTPStatusError as e:
66
+ if e.response.status_code == 404:
67
+ pass
68
+ else:
69
+ ui.error(f"Error wiping graph ({e.response.status_code})")
70
+ raise typer.Exit(1)
71
+
72
+ if nodes_deleted:
73
+ ui.info(f"Deleted {nodes_deleted} nodes")
74
+
75
+ should_ignore = build_ignore_matcher(directory, extra_patterns=ignore)
76
+ sync_service = SyncService(config, directory, log=lambda msg: ui.dim(f" {msg}"))
77
+
78
+ sync_service.initial_scan(directory, should_ignore)
79
+ sync_service.close()
80
+
81
+ ui.console.print()
82
+ ui.success("Reindex complete!")
83
+ if nodes_deleted > 0:
84
+ ui.dim(f" Old nodes deleted: {nodes_deleted}")
85
+ ui.console.print()
86
+ ui.dim("Run 'nogic watch' to continue monitoring for changes.")
87
+
88
+ finally:
89
+ client.close()
@@ -1,10 +1,8 @@
1
1
  """Status command - show project status and verify configuration."""
2
2
 
3
- import json
4
- import sys
5
3
  from pathlib import Path
6
4
  from datetime import datetime
7
- from typing import Annotated, Optional
5
+ from typing import Annotated
8
6
 
9
7
  import httpx
10
8
  import typer
@@ -17,28 +15,19 @@ from nogic import ui
17
15
 
18
16
  def status(
19
17
  directory: Annotated[Path, typer.Argument(help="Path to the project directory.")] = Path("."),
20
- format: Annotated[Optional[str], typer.Option("--format", help="Output format: text or json.")] = None,
21
18
  ):
22
19
  """Show project status and verify configuration."""
23
20
  directory = directory.resolve()
24
21
  nogic_dir = directory / CONFIG_DIR
25
22
  current_dir_hash = get_directory_hash(str(directory))
26
- json_mode = format == "json"
27
23
 
28
24
  if not nogic_dir.exists():
29
- if json_mode:
30
- _emit_json_status(error="Not a Nogic project.")
31
- else:
32
- ui.error("Not a Nogic project.")
33
- ui.dim("Run `nogic init` to initialize your project.")
25
+ ui.error("Not a Nogic project.")
26
+ ui.dim("Run `nogic init` to initialize your project.")
34
27
  raise typer.Exit(1)
35
28
 
36
29
  config = Config.load(directory)
37
30
 
38
- if json_mode:
39
- _status_json(directory, config, current_dir_hash)
40
- return
41
-
42
31
  if is_dev_mode():
43
32
  ui.dev_banner(get_api_url())
44
33
 
@@ -106,57 +95,6 @@ def status(
106
95
  ui.console.print()
107
96
 
108
97
 
109
- def _status_json(directory: Path, config: Config, dir_hash: str):
110
- """Output status as a single JSON object to stdout."""
111
- result: dict = {
112
- "project_name": config.project_name or None,
113
- "project_id": config.project_id or None,
114
- "directory": str(directory),
115
- "logged_in": bool(config.api_key),
116
- "backend": None,
117
- }
118
-
119
- if config.api_key and config.project_id:
120
- client = NogicClient(config)
121
- try:
122
- backend_project = client.get_project_by_directory(dir_hash)
123
- if backend_project:
124
- result["backend"] = {
125
- "status": "connected",
126
- "project_name": backend_project.name,
127
- "project_id": backend_project.id,
128
- "created_at": backend_project.created_at,
129
- "updated_at": backend_project.updated_at,
130
- }
131
- else:
132
- result["backend"] = {"status": "not_found"}
133
- except httpx.HTTPStatusError as e:
134
- result["backend"] = {
135
- "status": "error",
136
- "error": f"HTTP {e.response.status_code}",
137
- }
138
- except httpx.RequestError as e:
139
- result["backend"] = {
140
- "status": "error",
141
- "error": str(e),
142
- }
143
- finally:
144
- client.close()
145
- elif not config.api_key:
146
- result["backend"] = {"status": "not_logged_in"}
147
- else:
148
- result["backend"] = {"status": "not_configured"}
149
-
150
- sys.stdout.write(json.dumps(result) + "\n")
151
- sys.stdout.flush()
152
-
153
-
154
- def _emit_json_status(**kwargs):
155
- """Emit a JSON status object with error info."""
156
- sys.stdout.write(json.dumps(kwargs) + "\n")
157
- sys.stdout.flush()
158
-
159
-
160
98
  def _format_datetime(dt_string: str) -> str:
161
99
  try:
162
100
  dt = datetime.fromisoformat(dt_string.replace("Z", "+00:00"))
@@ -14,13 +14,11 @@ from nogic import telemetry, ui
14
14
  def sync(
15
15
  directory: Annotated[Path, typer.Argument(help="Path to the directory to sync.")] = Path("."),
16
16
  ignore: Annotated[Optional[list[str]], typer.Option("--ignore", help="Patterns to ignore.")] = None,
17
- format: Annotated[Optional[str], typer.Option("--format", help="Output format: text or json.")] = None,
18
17
  ):
19
18
  """One-time sync of a directory to backend."""
20
19
  directory = directory.resolve()
21
20
  nogic_dir = directory / ".nogic"
22
21
  ignore = ignore or []
23
- json_mode = format == "json"
24
22
 
25
23
  if not nogic_dir.exists():
26
24
  ui.error("Not a Nogic project.")
@@ -39,22 +37,22 @@ def sync(
39
37
  ui.dim("Run `nogic init` to initialize your project.")
40
38
  raise typer.Exit(1)
41
39
 
42
- if not json_mode:
43
- if is_dev_mode():
44
- ui.dev_banner(get_api_url())
45
- ui.banner("nogic sync", str(directory))
46
- ui.kv("Project", f"{config.project_id[:8]}...")
40
+ if is_dev_mode():
41
+ ui.dev_banner(get_api_url())
47
42
 
48
- log_fn = (lambda msg: None) if json_mode else (lambda msg: ui.dim(f" {msg}"))
49
- sync_service = SyncService(config, directory, log=log_fn, json_mode=json_mode)
43
+ ui.banner("nogic sync", str(directory))
44
+ ui.kv("Project", f"{config.project_id[:8]}...")
45
+
46
+ should_ignore = build_ignore_matcher(directory, extra_patterns=ignore)
47
+
48
+ sync_service = SyncService(config, directory, log=lambda msg: ui.dim(f" {msg}"))
50
49
 
51
50
  try:
52
- sync_service.initial_scan(directory, build_ignore_matcher(directory, extra_patterns=ignore))
51
+ sync_service.initial_scan(directory, should_ignore)
53
52
  telemetry.capture("cli_sync", {"status": "success"})
54
53
  except KeyboardInterrupt:
55
- if not json_mode:
56
- ui.console.print()
57
- ui.dim("Interrupted. Cleaning up...")
54
+ ui.console.print()
55
+ ui.dim("Interrupted. Cleaning up...")
58
56
  try:
59
57
  sync_service.client.clear_staging(config.project_id)
60
58
  except Exception:
@@ -67,6 +65,5 @@ def sync(
67
65
  finally:
68
66
  sync_service.close()
69
67
 
70
- if not json_mode:
71
- ui.console.print()
72
- ui.success("Done.")
68
+ ui.console.print()
69
+ ui.success("Done.")
@@ -0,0 +1,116 @@
1
+ """Watch command for file syncing."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Annotated, Optional
6
+
7
+ import typer
8
+
9
+ from nogic.config import Config, is_dev_mode, get_api_url
10
+ from nogic.ignore import build_ignore_matcher
11
+ from nogic.watcher import FileMonitor, SyncService
12
+ from nogic import ui
13
+
14
+
15
+ def watch(
16
+ directory: Annotated[Path, typer.Argument(help="Path to the directory to watch.")] = Path("."),
17
+ ignore: Annotated[Optional[list[str]], typer.Option("--ignore", help="Patterns to ignore.")] = None,
18
+ ):
19
+ """Watch a directory for file changes and sync to backend."""
20
+ directory = directory.resolve()
21
+ nogic_dir = directory / ".nogic"
22
+ ignore = ignore or []
23
+
24
+ if not nogic_dir.exists():
25
+ ui.error("Not a Nogic project.")
26
+ ui.dim("Run `nogic init` to initialize your project.")
27
+ raise typer.Exit(1)
28
+
29
+ config = Config.load(directory)
30
+
31
+ if not config.api_key:
32
+ ui.error("Not logged in.")
33
+ ui.dim("Run `nogic login` to authenticate.")
34
+ raise typer.Exit(1)
35
+
36
+ if not config.project_id:
37
+ ui.error("No project configured.")
38
+ ui.dim("Run `nogic init` to initialize your project.")
39
+ raise typer.Exit(1)
40
+
41
+ if is_dev_mode():
42
+ ui.dev_banner(get_api_url())
43
+
44
+ ui.banner("nogic watch", str(directory))
45
+ ui.kv("Project", f"{config.project_id[:8]}...")
46
+
47
+ sync_service = SyncService(config, directory, log=lambda msg: ui.dim(f" {msg}"))
48
+
49
+ should_ignore = build_ignore_matcher(directory, extra_patterns=ignore)
50
+
51
+ # Initial scan
52
+ try:
53
+ sync_service.initial_scan(directory, should_ignore)
54
+ except KeyboardInterrupt:
55
+ ui.console.print()
56
+ ui.dim("Interrupted during initial scan. Cleaning up...")
57
+ try:
58
+ sync_service.client.clear_staging(config.project_id)
59
+ except Exception:
60
+ pass
61
+ sync_service.close()
62
+ raise typer.Exit(1)
63
+
64
+ def on_change(path: Path):
65
+ try:
66
+ rel = path.relative_to(directory)
67
+ except ValueError:
68
+ return
69
+ try:
70
+ if sync_service.sync_file_immediate(path):
71
+ ui.console.print(f" [green]SYNCED[/] {rel}")
72
+ except Exception as e:
73
+ err_msg = str(e)
74
+ if "413" in err_msg:
75
+ ui.console.print(f" [yellow]SKIP[/] {rel} (file too large for API)")
76
+ elif "503" in err_msg or "502" in err_msg:
77
+ ui.console.print(f" [red]ERROR[/] {rel} (backend unavailable, will sync on next change)")
78
+ else:
79
+ ui.console.print(f" [red]ERROR[/] {rel}: {err_msg[:120]}")
80
+
81
+ def on_delete(path: Path):
82
+ try:
83
+ rel = path.relative_to(directory)
84
+ except ValueError:
85
+ return
86
+ try:
87
+ if sync_service.delete_file_immediate(path):
88
+ ui.console.print(f" [red]DELETED[/] {rel}")
89
+ else:
90
+ ui.console.print(f" [dim]DELETED[/] {rel} (not indexed)")
91
+ except Exception as e:
92
+ ui.console.print(f" [red]DELETED[/] {rel} (error: {str(e)[:80]})")
93
+
94
+ monitor = FileMonitor(
95
+ root_path=directory,
96
+ on_change=on_change,
97
+ on_delete=on_delete,
98
+ should_ignore=should_ignore,
99
+ )
100
+
101
+ ui.console.print()
102
+ ui.info("Watching for changes... (Ctrl+C to stop)")
103
+ ui.console.print()
104
+
105
+ monitor.start()
106
+ try:
107
+ while monitor.is_alive():
108
+ time.sleep(1)
109
+ except KeyboardInterrupt:
110
+ ui.console.print()
111
+ ui.dim("Stopping...")
112
+ finally:
113
+ monitor.stop()
114
+ sync_service.close()
115
+
116
+ ui.success("Done.")