knowcode 0.1.0__py3-none-any.whl
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.
- knowcode-0.1.0.dist-info/METADATA +175 -0
- knowcode-0.1.0.dist-info/RECORD +63 -0
- knowcode-0.1.0.dist-info/WHEEL +4 -0
- knowcode-0.1.0.dist-info/entry_points.txt +2 -0
- runtime/__init__.py +4 -0
- runtime/artifact/__init__.py +1 -0
- runtime/artifact/builder.py +179 -0
- runtime/cli/__init__.py +1 -0
- runtime/cli/animation.py +278 -0
- runtime/cli/app.py +309 -0
- runtime/cli/auth.py +171 -0
- runtime/cli/telemetry.py +91 -0
- runtime/exceptions/__init__.py +1 -0
- runtime/exceptions/errors.py +99 -0
- runtime/repository/__init__.py +13 -0
- runtime/repository/discovery.py +64 -0
- runtime/repository/models.py +103 -0
- runtime/repository/paths.py +50 -0
- runtime/repository/validator.py +100 -0
- runtime/services/__init__.py +1 -0
- runtime/services/ingest_service.py +105 -0
- runtime/services/init_service.py +45 -0
- runtime/services/semantic_sync_service.py +55 -0
- runtime/services/status_service.py +40 -0
- runtime/services/sync_service.py +57 -0
- runtime/templates/KNOWCODE_LOADER.md.j2 +24 -0
- runtime/templates/README_KNOWLEDGE.md.j2 +12 -0
- runtime/templates/README_STRUCTURE.md.j2 +19 -0
- runtime/templates/__init__.py +1 -0
- runtime/templates/active_context.md.j2 +3 -0
- runtime/templates/ingest_legacy.md.j2 +15 -0
- runtime/templates/raw_readme.md.j2 +9 -0
- runtime/templates/sync_reconciliation.md.j2 +17 -0
- runtime/templates/synthesize_knowledge.md.j2 +32 -0
- runtime/templates/track_intent.md.j2 +14 -0
- structural_engine/__init__.py +3 -0
- structural_engine/diff/__init__.py +1 -0
- structural_engine/diff/generator.py +92 -0
- structural_engine/diff/models.py +48 -0
- structural_engine/engine.py +192 -0
- structural_engine/logs/__init__.py +1 -0
- structural_engine/logs/generator.py +33 -0
- structural_engine/parser/__init__.py +7 -0
- structural_engine/parser/discovery.py +165 -0
- structural_engine/parser/extractors/base.py +44 -0
- structural_engine/parser/languages/javascript/adapter.py +149 -0
- structural_engine/parser/languages/python/adapter.py +174 -0
- structural_engine/parser/languages/typescript/adapter.py +165 -0
- structural_engine/parser/models.py +186 -0
- structural_engine/parser/parser.py +160 -0
- structural_engine/parser/resolvers/calls.py +105 -0
- structural_engine/parser/tree_sitter/registry.py +61 -0
- structural_engine/reports/__init__.py +1 -0
- structural_engine/reports/generator.py +77 -0
- structural_engine/results.py +54 -0
- structural_engine/revisions/__init__.py +1 -0
- structural_engine/revisions/tracker.py +32 -0
- structural_engine/snapshot/__init__.py +1 -0
- structural_engine/snapshot/generator.py +58 -0
- structural_engine/snapshot/loader.py +59 -0
- structural_engine/state/__init__.py +1 -0
- structural_engine/state/manager.py +169 -0
- structural_engine/state/models.py +34 -0
runtime/cli/animation.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Logo animation module for KnowCode CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
if sys.stdout.encoding != "utf-8":
|
|
13
|
+
try:
|
|
14
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
HIDE_CURSOR = "\033[?25l"
|
|
19
|
+
SHOW_CURSOR = "\033[?25h"
|
|
20
|
+
BOLD = "\033[1m"
|
|
21
|
+
DIM = "\033[2m"
|
|
22
|
+
RESET = "\033[0m"
|
|
23
|
+
WHITE = "\033[97m"
|
|
24
|
+
GRAY = "\033[90m"
|
|
25
|
+
|
|
26
|
+
LOGO_LINES = [
|
|
27
|
+
"███████████████████████████",
|
|
28
|
+
"██ ██",
|
|
29
|
+
"██ ███████████████ ██",
|
|
30
|
+
"██ █████▀ ▀█████ ██",
|
|
31
|
+
"██ █████▄ ▄█████ ██",
|
|
32
|
+
"██ ███████ ███████ ██",
|
|
33
|
+
"██ █████▀ ▀█████ ██",
|
|
34
|
+
"██ █████▄ ▄█████ ██",
|
|
35
|
+
"██ ███████████████ ██",
|
|
36
|
+
"██ ██",
|
|
37
|
+
"███████████████████████████",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
LOGO_HEIGHT = len(LOGO_LINES)
|
|
41
|
+
LOGO_WIDTH = len(LOGO_LINES[0])
|
|
42
|
+
LEFT_BLOCK_WIDTH = LOGO_WIDTH + 6
|
|
43
|
+
|
|
44
|
+
LXN = LOGO_WIDTH - 1
|
|
45
|
+
LYN = LOGO_HEIGHT - 1
|
|
46
|
+
TAGLINE = "KnowCode"
|
|
47
|
+
VERSION = "v0.1.0"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def move_up(n: int) -> None:
|
|
51
|
+
if n > 0:
|
|
52
|
+
sys.stdout.write(f"\033[{n}A")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def write(s: str) -> None:
|
|
56
|
+
sys.stdout.write(s)
|
|
57
|
+
sys.stdout.flush()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def visible_len(s: str) -> int:
|
|
64
|
+
"""Calculate the printed length of a string, excluding ANSI escape sequences."""
|
|
65
|
+
return len(ANSI_ESCAPE.sub("", s))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pad_left(s: str, width: int) -> str:
|
|
69
|
+
"""Pad a string to a specific visible width by appending spaces."""
|
|
70
|
+
v_len = visible_len(s)
|
|
71
|
+
if v_len < width:
|
|
72
|
+
return s + " " * (width - v_len)
|
|
73
|
+
return s
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BackgroundAnimator:
|
|
77
|
+
"""Animate the KnowCode logo in the background while tasks execute."""
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
self.status = "Starting..."
|
|
81
|
+
self.running = False
|
|
82
|
+
self._thread = None
|
|
83
|
+
self.tagline_visible_chars = 0
|
|
84
|
+
self.connector_visible_chars = 0
|
|
85
|
+
self.last_height = None
|
|
86
|
+
self.is_tty = sys.stdout.isatty()
|
|
87
|
+
self.phase = 0
|
|
88
|
+
|
|
89
|
+
def start(self) -> None:
|
|
90
|
+
"""Start the animation in a background thread if running in an interactive terminal."""
|
|
91
|
+
if not self.is_tty:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
write(HIDE_CURSOR)
|
|
95
|
+
|
|
96
|
+
cols, lines = shutil.get_terminal_size()
|
|
97
|
+
|
|
98
|
+
# Reserve screen lines initially depending on terminal size
|
|
99
|
+
if lines < LOGO_HEIGHT + 2 or cols < 74:
|
|
100
|
+
self.last_height = 0
|
|
101
|
+
else:
|
|
102
|
+
write("\n" * (LOGO_HEIGHT + 1))
|
|
103
|
+
self.last_height = LOGO_HEIGHT + 1
|
|
104
|
+
|
|
105
|
+
self.running = True
|
|
106
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
107
|
+
self._thread.start()
|
|
108
|
+
|
|
109
|
+
def stop(
|
|
110
|
+
self, final_status: str = "Ready", results: list[str] | None = None
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Stop the animation and print the final static state."""
|
|
113
|
+
if not self.is_tty:
|
|
114
|
+
if results:
|
|
115
|
+
for r_line in results:
|
|
116
|
+
write(f"{r_line}\n")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if self.running:
|
|
120
|
+
self.status = final_status
|
|
121
|
+
|
|
122
|
+
# Enforce a minimum animation duration (up to phase 40, approx 1.4s)
|
|
123
|
+
# so that the circular reveal animation has time to be visible
|
|
124
|
+
while self.phase < 40:
|
|
125
|
+
time.sleep(0.05)
|
|
126
|
+
|
|
127
|
+
self.running = False
|
|
128
|
+
if self._thread:
|
|
129
|
+
self._thread.join()
|
|
130
|
+
|
|
131
|
+
# Reposition to the top of our last frame before printing final state
|
|
132
|
+
if self.last_height and self.last_height > 0:
|
|
133
|
+
move_up(self.last_height)
|
|
134
|
+
write("\033[J")
|
|
135
|
+
|
|
136
|
+
# Print final clean state
|
|
137
|
+
self._print_final(results)
|
|
138
|
+
|
|
139
|
+
write(SHOW_CURSOR)
|
|
140
|
+
|
|
141
|
+
def _get_colored_char(self, char: str, x: int, y: int, R: float) -> str:
|
|
142
|
+
if char == " ":
|
|
143
|
+
return " "
|
|
144
|
+
|
|
145
|
+
dx = (x - LXN / 2) * 0.6
|
|
146
|
+
dy = y - LYN / 2
|
|
147
|
+
d = math.sqrt(dx * dx + dy * dy)
|
|
148
|
+
|
|
149
|
+
if d > R:
|
|
150
|
+
return " "
|
|
151
|
+
elif R - 1.5 < d <= R:
|
|
152
|
+
theta = math.atan2(dy, dx)
|
|
153
|
+
hue_idx = int(((theta + math.pi) / (2 * math.pi)) * 6) % 6
|
|
154
|
+
colors = [
|
|
155
|
+
"\033[1;91m",
|
|
156
|
+
"\033[1;93m",
|
|
157
|
+
"\033[1;92m",
|
|
158
|
+
"\033[1;96m",
|
|
159
|
+
"\033[1;94m",
|
|
160
|
+
"\033[1;95m",
|
|
161
|
+
]
|
|
162
|
+
return f"{colors[hue_idx]}{char}{RESET}"
|
|
163
|
+
else:
|
|
164
|
+
return f"{BOLD}{WHITE}{char}{RESET}"
|
|
165
|
+
|
|
166
|
+
def _run(self) -> None:
|
|
167
|
+
self.phase = 0
|
|
168
|
+
R_max = 12.5
|
|
169
|
+
cycle = 80
|
|
170
|
+
|
|
171
|
+
while self.running:
|
|
172
|
+
cols, lines = shutil.get_terminal_size()
|
|
173
|
+
|
|
174
|
+
# Determine display mode based on terminal size
|
|
175
|
+
if lines < LOGO_HEIGHT + 2 or cols < 74:
|
|
176
|
+
mode = "single"
|
|
177
|
+
current_height = 0
|
|
178
|
+
else:
|
|
179
|
+
mode = "full"
|
|
180
|
+
current_height = LOGO_HEIGHT + 1
|
|
181
|
+
|
|
182
|
+
# Move cursor back to the top of the previous frame
|
|
183
|
+
if self.last_height and self.last_height > 0:
|
|
184
|
+
move_up(self.last_height)
|
|
185
|
+
if current_height < self.last_height:
|
|
186
|
+
write("\033[J")
|
|
187
|
+
|
|
188
|
+
if mode == "single":
|
|
189
|
+
max_width = max(10, cols - 6)
|
|
190
|
+
status_text = self.status
|
|
191
|
+
if len(status_text) > max_width:
|
|
192
|
+
status_text = status_text[: max_width - 3] + "..."
|
|
193
|
+
write(f"\r ● {status_text:<{max_width}}")
|
|
194
|
+
else:
|
|
195
|
+
s = self.phase % cycle
|
|
196
|
+
R = R_max * (1 - math.cos(s * math.pi / 40)) / 2
|
|
197
|
+
|
|
198
|
+
left_lines = []
|
|
199
|
+
for y, line in enumerate(LOGO_LINES):
|
|
200
|
+
animated_line_chars = []
|
|
201
|
+
for x, char in enumerate(line):
|
|
202
|
+
animated_line_chars.append(
|
|
203
|
+
self._get_colored_char(char, x, y, R)
|
|
204
|
+
)
|
|
205
|
+
left_lines.append(f" {''.join(animated_line_chars)}")
|
|
206
|
+
|
|
207
|
+
# Tagline and active status side-by-side components on the right
|
|
208
|
+
right_width = cols - LEFT_BLOCK_WIDTH - 4
|
|
209
|
+
status_max = max(15, right_width - 2)
|
|
210
|
+
status_text = self.status
|
|
211
|
+
if len(status_text) > status_max:
|
|
212
|
+
status_text = status_text[: status_max - 3] + "..."
|
|
213
|
+
|
|
214
|
+
separator_len = min(31, right_width)
|
|
215
|
+
right_lines = [
|
|
216
|
+
f"{GRAY}{TAGLINE} {DIM}{GRAY}{VERSION}{RESET}",
|
|
217
|
+
f"{GRAY}{'─' * separator_len}{RESET}",
|
|
218
|
+
"",
|
|
219
|
+
f"{WHITE}● {status_text:<{status_max}}{RESET}",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
buffer = ["\n"]
|
|
223
|
+
max_len = max(len(left_lines), len(right_lines))
|
|
224
|
+
for i in range(max_len):
|
|
225
|
+
left = (
|
|
226
|
+
left_lines[i] if i < len(left_lines) else " " * LEFT_BLOCK_WIDTH
|
|
227
|
+
)
|
|
228
|
+
right = right_lines[i] if i < len(right_lines) else ""
|
|
229
|
+
buffer.append(f"{pad_left(left, LEFT_BLOCK_WIDTH)}{right}\n")
|
|
230
|
+
|
|
231
|
+
write("".join(buffer))
|
|
232
|
+
|
|
233
|
+
self.last_height = current_height
|
|
234
|
+
self.phase += 1
|
|
235
|
+
time.sleep(0.035)
|
|
236
|
+
|
|
237
|
+
def _print_final(self, results: list[str] | None = None) -> None:
|
|
238
|
+
cols, lines = shutil.get_terminal_size()
|
|
239
|
+
|
|
240
|
+
if lines < LOGO_HEIGHT + 4 or cols < 80:
|
|
241
|
+
max_width = max(10, cols - 6)
|
|
242
|
+
status_text = self.status
|
|
243
|
+
if len(status_text) > max_width:
|
|
244
|
+
status_text = status_text[: max_width - 3] + "..."
|
|
245
|
+
write(f"\r {WHITE}✔ {status_text:<{max_width}}{RESET}\n")
|
|
246
|
+
if results:
|
|
247
|
+
for r_line in results:
|
|
248
|
+
write(f"{r_line}\n")
|
|
249
|
+
else:
|
|
250
|
+
left_lines = []
|
|
251
|
+
for line in LOGO_LINES:
|
|
252
|
+
left_lines.append(f" {BOLD}{WHITE}{line}{RESET}")
|
|
253
|
+
|
|
254
|
+
right_width = cols - LEFT_BLOCK_WIDTH - 4
|
|
255
|
+
status_max = max(15, right_width - 2)
|
|
256
|
+
status_text = self.status
|
|
257
|
+
if len(status_text) > status_max:
|
|
258
|
+
status_text = status_text[: status_max - 3] + "..."
|
|
259
|
+
|
|
260
|
+
separator_len = min(31, right_width)
|
|
261
|
+
right_lines = [
|
|
262
|
+
f"{GRAY}{TAGLINE} {DIM}{GRAY}{VERSION}{RESET}",
|
|
263
|
+
f"{GRAY}{'─' * separator_len}{RESET}",
|
|
264
|
+
"",
|
|
265
|
+
f"{WHITE}✔ {status_text:<{status_max}}{RESET}",
|
|
266
|
+
"",
|
|
267
|
+
]
|
|
268
|
+
if results:
|
|
269
|
+
right_lines.extend(results)
|
|
270
|
+
|
|
271
|
+
buffer = ["\n"]
|
|
272
|
+
max_len = max(len(left_lines), len(right_lines))
|
|
273
|
+
for i in range(max_len):
|
|
274
|
+
left = left_lines[i] if i < len(left_lines) else " " * LEFT_BLOCK_WIDTH
|
|
275
|
+
right = right_lines[i] if i < len(right_lines) else ""
|
|
276
|
+
buffer.append(f"{pad_left(left, LEFT_BLOCK_WIDTH)}{right}\n")
|
|
277
|
+
|
|
278
|
+
write("".join(buffer))
|
runtime/cli/app.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""KnowCode CLI.
|
|
2
|
+
|
|
3
|
+
The top-level Typer application providing the ``know`` command.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from runtime.exceptions.errors import KnowcodeError
|
|
18
|
+
from runtime.services.init_service import run_init
|
|
19
|
+
from runtime.services.status_service import run_status
|
|
20
|
+
from runtime.services.sync_service import run_sync
|
|
21
|
+
from runtime.cli.animation import BackgroundAnimator
|
|
22
|
+
from runtime.cli.auth import ensure_authenticated
|
|
23
|
+
from runtime.cli.telemetry import send_telemetry_async
|
|
24
|
+
import shutil
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
name="knowcode",
|
|
28
|
+
help="KnowCode Structural Cognition Engine",
|
|
29
|
+
add_completion=False,
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
console = Console()
|
|
34
|
+
animator = BackgroundAnimator()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _capture_console_lines(renderable) -> list[str]:
|
|
38
|
+
"""Capture rich console output and split it into clean string lines constrained to right column."""
|
|
39
|
+
if sys.stdout.isatty():
|
|
40
|
+
cols, lines = shutil.get_terminal_size()
|
|
41
|
+
if cols >= 80 and lines >= 14:
|
|
42
|
+
right_width = max(20, cols - 35 - 4)
|
|
43
|
+
else:
|
|
44
|
+
right_width = max(20, cols - 4)
|
|
45
|
+
from rich.console import Console as RichConsole
|
|
46
|
+
|
|
47
|
+
temp_console = RichConsole(width=right_width, color_system=console.color_system)
|
|
48
|
+
else:
|
|
49
|
+
temp_console = console
|
|
50
|
+
|
|
51
|
+
with temp_console.capture() as capture:
|
|
52
|
+
temp_console.print(renderable)
|
|
53
|
+
return capture.get().rstrip().split("\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _update_animator(logger, method_name, event_dict):
|
|
57
|
+
if animator and animator.running:
|
|
58
|
+
animator.status = str(event_dict.get("event", "Processing..."))
|
|
59
|
+
return event_dict
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _setup_logging() -> None:
|
|
63
|
+
"""Configure minimal JSON logging for the entire ecosystem."""
|
|
64
|
+
structlog.configure(
|
|
65
|
+
processors=[
|
|
66
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
67
|
+
structlog.processors.JSONRenderer(),
|
|
68
|
+
],
|
|
69
|
+
logger_factory=structlog.PrintLoggerFactory(
|
|
70
|
+
file=open("knowcode.log", "a", encoding="utf-8")
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_setup_logging()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _handle_error(e: Exception) -> None:
|
|
79
|
+
"""Print an error nicely and exit."""
|
|
80
|
+
if isinstance(e, KnowcodeError):
|
|
81
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
82
|
+
else:
|
|
83
|
+
console.print(f"[bold red]Unexpected Error:[/bold red] {e}")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command(".")
|
|
88
|
+
def init_command() -> None:
|
|
89
|
+
"""Initialize a new KnowCode instance in the current repository."""
|
|
90
|
+
try:
|
|
91
|
+
ensure_authenticated()
|
|
92
|
+
except Exception as e:
|
|
93
|
+
_handle_error(e)
|
|
94
|
+
|
|
95
|
+
animator.start()
|
|
96
|
+
try:
|
|
97
|
+
cwd = Path.cwd()
|
|
98
|
+
result = run_init(cwd)
|
|
99
|
+
panel = Panel.fit(
|
|
100
|
+
f"[bold green]{result.message}[/bold green]\n"
|
|
101
|
+
f"Structural Revision: [cyan]{result.structural_revision}[/cyan]\n"
|
|
102
|
+
f"Initial Snapshot: [cyan]{result.snapshot_file}[/cyan]\n\n"
|
|
103
|
+
f"[bold yellow]Next Step:[/bold yellow] Type [magenta]/knowcode[/magenta] in your AI chat to activate governance.",
|
|
104
|
+
title="KnowCode Initialization",
|
|
105
|
+
border_style="green",
|
|
106
|
+
)
|
|
107
|
+
results_lines = _capture_console_lines(panel)
|
|
108
|
+
animator.stop(final_status="Initialization complete.", results=results_lines)
|
|
109
|
+
send_telemetry_async("init", "success")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
animator.stop(final_status="Initialization failed.")
|
|
112
|
+
send_telemetry_async("init", "failed")
|
|
113
|
+
_handle_error(e)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("init")
|
|
117
|
+
def init_alias_command() -> None:
|
|
118
|
+
"""Initialize a new KnowCode instance in the current repository."""
|
|
119
|
+
init_command()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command("sync")
|
|
123
|
+
def sync_command() -> None:
|
|
124
|
+
"""Synchronize the KnowCode artifact with the current repository state."""
|
|
125
|
+
try:
|
|
126
|
+
ensure_authenticated()
|
|
127
|
+
except Exception as e:
|
|
128
|
+
_handle_error(e)
|
|
129
|
+
|
|
130
|
+
animator.start()
|
|
131
|
+
try:
|
|
132
|
+
cwd = Path.cwd()
|
|
133
|
+
result = run_sync(cwd)
|
|
134
|
+
|
|
135
|
+
if not result.changes_detected:
|
|
136
|
+
results_lines = _capture_console_lines(f"[green]✓[/green] {result.message}")
|
|
137
|
+
animator.stop(final_status="Sync complete.", results=results_lines)
|
|
138
|
+
send_telemetry_async("sync", "success")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
table = Table(title="Sync Results", show_header=False)
|
|
142
|
+
table.add_column("Key", style="dim")
|
|
143
|
+
table.add_column("Value", style="cyan")
|
|
144
|
+
|
|
145
|
+
table.add_row("Revision", result.structural_revision)
|
|
146
|
+
table.add_row("Snapshot", result.snapshot_file)
|
|
147
|
+
table.add_row("Report", result.report_file)
|
|
148
|
+
table.add_row(
|
|
149
|
+
"Components",
|
|
150
|
+
", ".join(result.affected_components)
|
|
151
|
+
if result.affected_components
|
|
152
|
+
else "None",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
panel = Panel.fit(
|
|
156
|
+
table, title="[bold green]Sync Complete[/bold green]", border_style="green"
|
|
157
|
+
)
|
|
158
|
+
results_lines = _capture_console_lines(panel)
|
|
159
|
+
animator.stop(final_status="Sync complete.", results=results_lines)
|
|
160
|
+
send_telemetry_async("sync", "success")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
animator.stop(final_status="Sync failed.")
|
|
164
|
+
send_telemetry_async("sync", "failed")
|
|
165
|
+
_handle_error(e)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command("status")
|
|
169
|
+
def status_command() -> None:
|
|
170
|
+
"""View the current status of the KnowCode instance."""
|
|
171
|
+
try:
|
|
172
|
+
ensure_authenticated()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
_handle_error(e)
|
|
175
|
+
|
|
176
|
+
animator.start()
|
|
177
|
+
try:
|
|
178
|
+
cwd = Path.cwd()
|
|
179
|
+
result = run_status(cwd)
|
|
180
|
+
|
|
181
|
+
table = Table(
|
|
182
|
+
title=f"KnowCode Status: {result.repository_root}", show_header=False
|
|
183
|
+
)
|
|
184
|
+
table.add_column("Key", style="dim")
|
|
185
|
+
table.add_column("Value", style="bold")
|
|
186
|
+
|
|
187
|
+
table.add_row(
|
|
188
|
+
"Initialized",
|
|
189
|
+
"[green]Yes[/green]" if result.initialized else "[red]No[/red]",
|
|
190
|
+
)
|
|
191
|
+
table.add_row(
|
|
192
|
+
"Structural Revision", f"[cyan]{result.structural_revision}[/cyan]"
|
|
193
|
+
)
|
|
194
|
+
table.add_row(
|
|
195
|
+
"Semantic Revision", f"[magenta]{result.semantic_revision}[/magenta]"
|
|
196
|
+
)
|
|
197
|
+
table.add_row("Current Snapshot", result.current_snapshot)
|
|
198
|
+
table.add_row("Latest Report", result.latest_report)
|
|
199
|
+
|
|
200
|
+
last_sync = (
|
|
201
|
+
result.last_sync.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
202
|
+
if result.last_sync
|
|
203
|
+
else "Never"
|
|
204
|
+
)
|
|
205
|
+
table.add_row("Last Sync", last_sync)
|
|
206
|
+
|
|
207
|
+
results_lines = _capture_console_lines(table)
|
|
208
|
+
animator.stop(final_status="Status retrieved.", results=results_lines)
|
|
209
|
+
send_telemetry_async("status", "success")
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
animator.stop(final_status="Failed to get status.")
|
|
213
|
+
send_telemetry_async("status", "failed")
|
|
214
|
+
_handle_error(e)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@app.command("sync-semantic")
|
|
218
|
+
def sync_semantic_command() -> None:
|
|
219
|
+
"""Commit semantic changes and flush the active memory buffer."""
|
|
220
|
+
try:
|
|
221
|
+
ensure_authenticated()
|
|
222
|
+
except Exception as e:
|
|
223
|
+
_handle_error(e)
|
|
224
|
+
|
|
225
|
+
animator.start()
|
|
226
|
+
try:
|
|
227
|
+
from runtime.services.semantic_sync_service import run_semantic_sync
|
|
228
|
+
|
|
229
|
+
cwd = Path.cwd()
|
|
230
|
+
result = run_semantic_sync(cwd)
|
|
231
|
+
|
|
232
|
+
table = Table(title="Semantic Sync Results", show_header=False)
|
|
233
|
+
table.add_column("Key", style="dim")
|
|
234
|
+
table.add_column("Value", style="magenta")
|
|
235
|
+
|
|
236
|
+
table.add_row("Revision", result.semantic_revision)
|
|
237
|
+
table.add_row("Memory", "[dim]Flushed previous_context.md[/dim]")
|
|
238
|
+
|
|
239
|
+
panel = Panel.fit(
|
|
240
|
+
table,
|
|
241
|
+
title=f"[bold magenta]✓ {result.message}[/bold magenta]",
|
|
242
|
+
border_style="magenta",
|
|
243
|
+
)
|
|
244
|
+
results_lines = _capture_console_lines(panel)
|
|
245
|
+
animator.stop(final_status="Semantic sync complete.", results=results_lines)
|
|
246
|
+
send_telemetry_async("sync-semantic", "success")
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
animator.stop(final_status="Semantic sync failed.")
|
|
250
|
+
send_telemetry_async("sync-semantic", "failed")
|
|
251
|
+
_handle_error(e)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command("ingest-semantic")
|
|
255
|
+
def ingest_semantic_command(
|
|
256
|
+
target_file: str = typer.Argument(
|
|
257
|
+
..., help="Path to the raw file to ingest and remove, or '.' to process all."
|
|
258
|
+
),
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Ingest a legacy document and bump the semantic revision."""
|
|
261
|
+
animator.start()
|
|
262
|
+
try:
|
|
263
|
+
from runtime.services.ingest_service import run_ingest
|
|
264
|
+
|
|
265
|
+
cwd = Path.cwd()
|
|
266
|
+
result = run_ingest(cwd, target_file)
|
|
267
|
+
animator.stop(final_status="Semantic ingest complete.")
|
|
268
|
+
|
|
269
|
+
if result.semantic_revision == "No change":
|
|
270
|
+
console.print(f"[yellow]ℹ {result.message}[/yellow]")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
table = Table(title="Ingest Results", show_header=False)
|
|
274
|
+
table.add_column("Key", style="dim")
|
|
275
|
+
table.add_column("Value", style="magenta")
|
|
276
|
+
|
|
277
|
+
table.add_row("Revision", result.semantic_revision)
|
|
278
|
+
files_str = (
|
|
279
|
+
", ".join(result.deleted_files)
|
|
280
|
+
if len(result.deleted_files) <= 3
|
|
281
|
+
else f"{len(result.deleted_files)} files"
|
|
282
|
+
)
|
|
283
|
+
table.add_row("Memory", f"[dim]Flushed {files_str} from inbox[/dim]")
|
|
284
|
+
|
|
285
|
+
console.print(
|
|
286
|
+
Panel.fit(
|
|
287
|
+
table,
|
|
288
|
+
title=f"[bold magenta]✓ {result.message}[/bold magenta]",
|
|
289
|
+
border_style="magenta",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
animator.stop(final_status="Semantic ingest failed.")
|
|
295
|
+
_handle_error(e)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.command("auth")
|
|
299
|
+
def auth_command() -> None:
|
|
300
|
+
"""Manage authentication and telemetry preferences."""
|
|
301
|
+
from runtime.cli.auth import manage_auth
|
|
302
|
+
try:
|
|
303
|
+
manage_auth()
|
|
304
|
+
except Exception as e:
|
|
305
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
app()
|