codebase-receipts-cli 1.0.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.
- codebase_receipts_cli-1.0.0.dist-info/METADATA +268 -0
- codebase_receipts_cli-1.0.0.dist-info/RECORD +78 -0
- codebase_receipts_cli-1.0.0.dist-info/WHEEL +4 -0
- codebase_receipts_cli-1.0.0.dist-info/entry_points.txt +2 -0
- codebase_receipts_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- receipts/__init__.py +0 -0
- receipts/ama/__init__.py +0 -0
- receipts/ama/interviewer.py +415 -0
- receipts/ats/__init__.py +1 -0
- receipts/ats/comparator.py +98 -0
- receipts/ats/scorer.py +550 -0
- receipts/ats/stats.py +164 -0
- receipts/cli.py +2062 -0
- receipts/config.py +106 -0
- receipts/errors.py +18 -0
- receipts/export.py +120 -0
- receipts/ingest/__init__.py +0 -0
- receipts/ingest/artifact_extractor.py +679 -0
- receipts/ingest/git_source.py +159 -0
- receipts/ingest/manifest.py +114 -0
- receipts/ingest/scanner.py +141 -0
- receipts/ingest/secrets_scanner.py +225 -0
- receipts/interactive/__init__.py +1 -0
- receipts/interactive/repl.py +755 -0
- receipts/ledger/__init__.py +0 -0
- receipts/ledger/pricing_table.py +54 -0
- receipts/ledger/token_ledger.py +226 -0
- receipts/llm/__init__.py +0 -0
- receipts/llm/anthropic_provider.py +95 -0
- receipts/llm/factory.py +124 -0
- receipts/llm/fake_provider.py +79 -0
- receipts/llm/gemini_provider.py +205 -0
- receipts/llm/json_utils.py +46 -0
- receipts/llm/metered.py +62 -0
- receipts/llm/ollama_provider.py +117 -0
- receipts/llm/openai_provider.py +118 -0
- receipts/llm/provider.py +42 -0
- receipts/llm/split.py +78 -0
- receipts/llm/token_estimate.py +35 -0
- receipts/mine/__init__.py +1 -0
- receipts/mine/code_metrics.py +246 -0
- receipts/mine/git_evidence.py +109 -0
- receipts/prep/__init__.py +1 -0
- receipts/prep/dossier.py +313 -0
- receipts/prep/readiness.py +227 -0
- receipts/resume/__init__.py +0 -0
- receipts/resume/claim_extractor.py +338 -0
- receipts/resume/loader.py +35 -0
- receipts/resume/pdf_parser.py +259 -0
- receipts/resume/tex_parser.py +394 -0
- receipts/rewrite/__init__.py +0 -0
- receipts/rewrite/compiler.py +180 -0
- receipts/rewrite/optimizer.py +140 -0
- receipts/rewrite/tex_rewriter.py +227 -0
- receipts/tui/__init__.py +0 -0
- receipts/tui/ama_app.py +454 -0
- receipts/tui/app.py +316 -0
- receipts/tui/dossier_app.py +170 -0
- receipts/tui/hub.py +367 -0
- receipts/tui/ingest_app.py +183 -0
- receipts/tui/rewrite_app.py +463 -0
- receipts/tui/score_app.py +237 -0
- receipts/tui/widgets/__init__.py +0 -0
- receipts/tui/widgets/claims_table.py +50 -0
- receipts/tui/widgets/diff_view.py +38 -0
- receipts/tui/widgets/status_bar.py +38 -0
- receipts/verify/__init__.py +0 -0
- receipts/verify/bm25.py +112 -0
- receipts/verify/enrich.py +128 -0
- receipts/verify/jd_fetch.py +101 -0
- receipts/verify/kb.py +379 -0
- receipts/verify/keyword_gap.py +127 -0
- receipts/verify/query_expand.py +88 -0
- receipts/verify/rerank.py +74 -0
- receipts/verify/rewriter.py +172 -0
- receipts/verify/router.py +211 -0
- receipts/verify/summaries.py +258 -0
- receipts/verify/verifier.py +552 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"""Interactive slash-command REPL — chat-style UX for Receipts.
|
|
2
|
+
|
|
3
|
+
A lightweight REPL using stdlib input() + typer echo for colors.
|
|
4
|
+
Every CLI subcommand has a slash equivalent: /check, /setup, /ingest,
|
|
5
|
+
/verify, /tui, /ama, /rewrite, /score, /dossier, /readiness, /export,
|
|
6
|
+
/ledger, /dashboard, plus /status, /help, /quit.
|
|
7
|
+
Fuzzy-matches mistyped commands and suggests the closest match.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import difflib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from receipts.errors import ReceiptsError
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Command registry
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_COMMANDS: dict[str, Command] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Command:
|
|
27
|
+
def __init__(self, name: str, aliases: list[str], help_text: str, handler):
|
|
28
|
+
self.name = name
|
|
29
|
+
self.aliases = aliases
|
|
30
|
+
self.help_text = help_text
|
|
31
|
+
self.handler = handler
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def all_names(self) -> list[str]:
|
|
35
|
+
return [self.name] + self.aliases
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _register(name: str, aliases: list[str], help_text: str):
|
|
39
|
+
def decorator(fn):
|
|
40
|
+
cmd = Command(name, aliases, help_text, fn)
|
|
41
|
+
_COMMANDS[name] = cmd
|
|
42
|
+
for alias in aliases:
|
|
43
|
+
_COMMANDS[alias] = cmd
|
|
44
|
+
return fn
|
|
45
|
+
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Helpers
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _echo(msg: str, color: str | None = None, bold: bool = False) -> None:
|
|
55
|
+
typer.secho(msg, fg=color, bold=bold)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _prompt(msg: str, default: str = "") -> str:
|
|
59
|
+
suffix = f" [{default}]" if default else ""
|
|
60
|
+
try:
|
|
61
|
+
val = input(f" {msg}{suffix}: ").strip()
|
|
62
|
+
except (EOFError, KeyboardInterrupt):
|
|
63
|
+
print()
|
|
64
|
+
return default
|
|
65
|
+
return val or default
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _prompt_path(msg: str, must_exist: bool = True) -> Path | None:
|
|
69
|
+
raw = _prompt(msg)
|
|
70
|
+
if not raw:
|
|
71
|
+
_echo(" (skipped)", color=typer.colors.YELLOW)
|
|
72
|
+
return None
|
|
73
|
+
p = Path(raw)
|
|
74
|
+
if must_exist and not p.exists():
|
|
75
|
+
_echo(f" Not found: {p}", color=typer.colors.RED)
|
|
76
|
+
return None
|
|
77
|
+
return p
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _prompt_int(msg: str, default: int = 5) -> int:
|
|
81
|
+
raw = _prompt(msg, str(default))
|
|
82
|
+
try:
|
|
83
|
+
return int(raw)
|
|
84
|
+
except ValueError:
|
|
85
|
+
return default
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _prompt_sections() -> list[str] | None:
|
|
89
|
+
raw = _prompt("Sections to filter (comma-separated, or Enter for all)")
|
|
90
|
+
if not raw:
|
|
91
|
+
return None
|
|
92
|
+
return [s.strip() for s in raw.split(",") if s.strip()]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _prompt_source() -> str | None:
|
|
96
|
+
"""Prompt for a codebase spec — local folder, git URL, or manifest alias.
|
|
97
|
+
|
|
98
|
+
Existence isn't checked here: URLs and aliases aren't paths. Resolution
|
|
99
|
+
(and the friendly known-aliases error) happens in the CLI backend.
|
|
100
|
+
"""
|
|
101
|
+
raw = _prompt("Codebase (folder, git URL, or alias — already ingested)")
|
|
102
|
+
if not raw:
|
|
103
|
+
_echo(" (skipped)", color=typer.colors.YELLOW)
|
|
104
|
+
return None
|
|
105
|
+
return raw
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _check_provider_ready() -> bool:
|
|
109
|
+
"""Quick check if the configured provider is reachable."""
|
|
110
|
+
try:
|
|
111
|
+
from receipts.config import load_settings
|
|
112
|
+
from receipts.llm.factory import get_provider
|
|
113
|
+
|
|
114
|
+
settings = load_settings()
|
|
115
|
+
get_provider(settings, verify=True)
|
|
116
|
+
return True
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
_echo(f" Provider not ready: {exc}", color=typer.colors.RED)
|
|
119
|
+
_echo(
|
|
120
|
+
" Check your .env config and that your provider is running.",
|
|
121
|
+
color=typer.colors.YELLOW,
|
|
122
|
+
)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# /help
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@_register("help", ["h", "?"], "Show available commands")
|
|
132
|
+
def _cmd_help(args: str) -> None:
|
|
133
|
+
_echo("")
|
|
134
|
+
_echo(" Available commands:", bold=True)
|
|
135
|
+
_echo("")
|
|
136
|
+
|
|
137
|
+
seen: set[str] = set()
|
|
138
|
+
for cmd in _COMMANDS.values():
|
|
139
|
+
if cmd.name in seen:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(cmd.name)
|
|
142
|
+
aliases_str = f" ({', '.join(cmd.aliases)})" if cmd.aliases else ""
|
|
143
|
+
_echo(f" /{cmd.name:<10}{aliases_str}")
|
|
144
|
+
_echo(f" {cmd.help_text}")
|
|
145
|
+
_echo("")
|
|
146
|
+
_echo(" Type a command, or just start typing — I'll suggest the right one.")
|
|
147
|
+
_echo(
|
|
148
|
+
" Verification here uses the default evidence threshold (0.5);"
|
|
149
|
+
" use the CLI's --threshold flag to tune it.",
|
|
150
|
+
color=typer.colors.BRIGHT_BLACK,
|
|
151
|
+
)
|
|
152
|
+
_echo("")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# /status
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@_register("status", ["s", "info"], "Show provider, KB stats, and token spend")
|
|
161
|
+
def _cmd_status(args: str) -> None:
|
|
162
|
+
from receipts.config import load_settings
|
|
163
|
+
from receipts.ledger.token_ledger import TokenLedger
|
|
164
|
+
|
|
165
|
+
_echo("")
|
|
166
|
+
_echo(" Status", bold=True)
|
|
167
|
+
_echo("")
|
|
168
|
+
|
|
169
|
+
settings = load_settings()
|
|
170
|
+
_echo(f" Provider: {settings.provider}")
|
|
171
|
+
model_attr = {
|
|
172
|
+
"ollama": "ollama_chat_model",
|
|
173
|
+
"gemini": "gemini_chat_model",
|
|
174
|
+
"anthropic": "anthropic_chat_model",
|
|
175
|
+
"openai": "openai_chat_model",
|
|
176
|
+
}.get(settings.provider)
|
|
177
|
+
model = getattr(settings, model_attr, None) if model_attr else None
|
|
178
|
+
_echo(f" Model: {model or '(default)'}")
|
|
179
|
+
|
|
180
|
+
ready = _check_provider_ready()
|
|
181
|
+
if ready:
|
|
182
|
+
_echo(" Connected: Yes", color=typer.colors.GREEN)
|
|
183
|
+
else:
|
|
184
|
+
_echo(" Connected: No", color=typer.colors.RED)
|
|
185
|
+
|
|
186
|
+
tl = TokenLedger()
|
|
187
|
+
try:
|
|
188
|
+
lt = tl.lifetime_summary()
|
|
189
|
+
finally:
|
|
190
|
+
tl.close()
|
|
191
|
+
|
|
192
|
+
if lt.call_count > 0:
|
|
193
|
+
_echo("")
|
|
194
|
+
_echo(f" LLM calls: {lt.call_count}")
|
|
195
|
+
_echo(f" Input tokens: {lt.total_input:,}")
|
|
196
|
+
_echo(f" Output tokens: {lt.total_output:,}")
|
|
197
|
+
_echo(f" Est. cost: ${lt.total_cost:.4f}")
|
|
198
|
+
else:
|
|
199
|
+
_echo(" No LLM calls recorded yet.")
|
|
200
|
+
_echo("")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# /check
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@_register(
|
|
209
|
+
"check",
|
|
210
|
+
["c", "connectivity"],
|
|
211
|
+
"Check provider configuration and connectivity (sends no content)",
|
|
212
|
+
)
|
|
213
|
+
def _cmd_check(args: str) -> None:
|
|
214
|
+
from receipts.cli import check as check_cmd
|
|
215
|
+
|
|
216
|
+
_echo("")
|
|
217
|
+
try:
|
|
218
|
+
check_cmd()
|
|
219
|
+
except (ReceiptsError, SystemExit, typer.Exit):
|
|
220
|
+
pass # the command already printed the failure in red
|
|
221
|
+
_echo("")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# /setup
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@_register(
|
|
230
|
+
"setup",
|
|
231
|
+
["init", "configure"],
|
|
232
|
+
"First-time setup wizard — pick a provider, store your key, validate",
|
|
233
|
+
)
|
|
234
|
+
def _cmd_setup(args: str) -> None:
|
|
235
|
+
from receipts.cli import setup as setup_cmd
|
|
236
|
+
|
|
237
|
+
_echo("")
|
|
238
|
+
try:
|
|
239
|
+
setup_cmd()
|
|
240
|
+
except (ReceiptsError, SystemExit, typer.Exit):
|
|
241
|
+
pass # the wizard already printed what went wrong
|
|
242
|
+
except (typer.Abort, EOFError, KeyboardInterrupt):
|
|
243
|
+
_echo(" Setup cancelled.", color=typer.colors.YELLOW)
|
|
244
|
+
_echo("")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# /ingest
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@_register("ingest", ["i", "index"], "Ingest a codebase into the knowledge base")
|
|
253
|
+
def _cmd_ingest(args: str) -> None:
|
|
254
|
+
_echo("")
|
|
255
|
+
_echo(" Ingest a codebase", bold=True)
|
|
256
|
+
_echo("")
|
|
257
|
+
|
|
258
|
+
if args:
|
|
259
|
+
source_path = Path(args)
|
|
260
|
+
else:
|
|
261
|
+
source_path = _prompt_path(
|
|
262
|
+
"Path to codebase (folder or git URL)", must_exist=False
|
|
263
|
+
)
|
|
264
|
+
if source_path is None:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
budget_raw = _prompt("Token budget (Enter for auto)")
|
|
268
|
+
token_budget = int(budget_raw) if budget_raw.strip().isdigit() else None
|
|
269
|
+
|
|
270
|
+
if not _check_provider_ready():
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
_echo("")
|
|
274
|
+
_echo(f" Ingesting: {source_path}")
|
|
275
|
+
budget_label = "auto" if token_budget is None else f"{token_budget:,} tokens"
|
|
276
|
+
_echo(f" Budget: {budget_label}")
|
|
277
|
+
_echo("")
|
|
278
|
+
|
|
279
|
+
from receipts.cli import _run_ingest
|
|
280
|
+
from receipts.ingest.git_source import cloned_repo, looks_like_git_url
|
|
281
|
+
|
|
282
|
+
source_str = str(source_path)
|
|
283
|
+
try:
|
|
284
|
+
if looks_like_git_url(source_str):
|
|
285
|
+
_echo(" Cloning...")
|
|
286
|
+
with cloned_repo(source_str) as root:
|
|
287
|
+
_run_ingest(root, token_budget)
|
|
288
|
+
else:
|
|
289
|
+
if not source_path.is_dir():
|
|
290
|
+
_echo(f" Not a folder: {source_path}", color=typer.colors.RED)
|
|
291
|
+
return
|
|
292
|
+
_run_ingest(source_path, token_budget)
|
|
293
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
294
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# /verify
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@_register("verify", ["v", "check-resume"], "Verify resume claims against the KB")
|
|
303
|
+
def _cmd_verify(args: str) -> None:
|
|
304
|
+
_echo("")
|
|
305
|
+
_echo(" Verify resume claims", bold=True)
|
|
306
|
+
_echo("")
|
|
307
|
+
|
|
308
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
309
|
+
if resume_path is None:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
source_spec = _prompt_source()
|
|
313
|
+
if source_spec is None:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
sections = _prompt_sections()
|
|
317
|
+
threshold = 0.5
|
|
318
|
+
|
|
319
|
+
do_rewrite = _prompt("Include rewrites? (y/n)", "n").lower().startswith("y")
|
|
320
|
+
jd_raw = _prompt("Job description file path (or Enter to skip)")
|
|
321
|
+
jd_path = jd_raw if jd_raw else None
|
|
322
|
+
|
|
323
|
+
if not _check_provider_ready():
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
_echo("")
|
|
327
|
+
from receipts.cli import _run_verify
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
_run_verify(resume_path, source_spec, do_rewrite, jd_path, threshold, sections)
|
|
331
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
332
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# /tui
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@_register("tui", ["t"], "Launch the Textual verify TUI (panelled, streaming)")
|
|
341
|
+
def _cmd_tui(args: str) -> None:
|
|
342
|
+
_echo("")
|
|
343
|
+
_echo(" Verify TUI", bold=True)
|
|
344
|
+
_echo("")
|
|
345
|
+
|
|
346
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
347
|
+
if resume_path is None:
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
source_spec = _prompt_source()
|
|
351
|
+
if source_spec is None:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
sections = _prompt_sections()
|
|
355
|
+
|
|
356
|
+
if not _check_provider_ready():
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
from receipts.cli import _resolve_entries, _resolve_kb_source
|
|
360
|
+
from receipts.tui.app import ReceiptsApp
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
kb_dir, _ = _resolve_kb_source(source_spec)
|
|
364
|
+
# Same entry picker the CLI uses: with no section filter it lists the
|
|
365
|
+
# project/experience entries and asks which match the ingested repo.
|
|
366
|
+
entries = _resolve_entries(resume_path, sections, None, False)
|
|
367
|
+
tui_app = ReceiptsApp(
|
|
368
|
+
resume_path,
|
|
369
|
+
Path(source_spec),
|
|
370
|
+
threshold=0.5,
|
|
371
|
+
sections=sections,
|
|
372
|
+
entries=entries,
|
|
373
|
+
kb_dir=kb_dir,
|
|
374
|
+
)
|
|
375
|
+
tui_app.run()
|
|
376
|
+
except (ReceiptsError, SystemExit, typer.Exit) as exc:
|
|
377
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# /ama
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@_register("ama", ["a", "interview", "mock"], "Start a mock interview session")
|
|
386
|
+
def _cmd_ama(args: str) -> None:
|
|
387
|
+
_echo("")
|
|
388
|
+
_echo(" Mock Interview (AMA)", bold=True)
|
|
389
|
+
_echo("")
|
|
390
|
+
|
|
391
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
392
|
+
if resume_path is None:
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
source_spec = _prompt_source()
|
|
396
|
+
if source_spec is None:
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
rounds = _prompt_int("Number of rounds", default=5)
|
|
400
|
+
sections = _prompt_sections()
|
|
401
|
+
|
|
402
|
+
use_tui = _prompt("Use TUI chat interface? (y/n)", "y").lower().startswith("y")
|
|
403
|
+
|
|
404
|
+
if not _check_provider_ready():
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
_echo("")
|
|
408
|
+
if use_tui:
|
|
409
|
+
from receipts.cli import _resolve_kb_source
|
|
410
|
+
from receipts.tui.ama_app import AmaApp
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
kb_dir, _ = _resolve_kb_source(source_spec)
|
|
414
|
+
except ReceiptsError as exc:
|
|
415
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
ama_app = AmaApp(
|
|
419
|
+
resume_path,
|
|
420
|
+
Path(source_spec),
|
|
421
|
+
n_rounds=rounds,
|
|
422
|
+
threshold=0.5,
|
|
423
|
+
sections=sections,
|
|
424
|
+
kb_dir=kb_dir,
|
|
425
|
+
)
|
|
426
|
+
ama_app.run()
|
|
427
|
+
else:
|
|
428
|
+
from receipts.cli import _run_ama
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
_run_ama(resume_path, source_spec, rounds, 0.5, sections)
|
|
432
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
433
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# /rewrite
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@_register(
|
|
442
|
+
"rewrite",
|
|
443
|
+
["r", "rw", "optimize"],
|
|
444
|
+
"Rewrite weak bullets and generate optimized .tex",
|
|
445
|
+
)
|
|
446
|
+
def _cmd_rewrite(args: str) -> None:
|
|
447
|
+
_echo("")
|
|
448
|
+
_echo(" ATS-Optimized Rewrite", bold=True)
|
|
449
|
+
_echo("")
|
|
450
|
+
|
|
451
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
452
|
+
if resume_path is None:
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
source_path = _prompt_path("Codebase folder (already ingested)")
|
|
456
|
+
if source_path is None:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
sections = _prompt_sections()
|
|
460
|
+
output_raw = _prompt("Output .tex path (or Enter for default)")
|
|
461
|
+
output_path = output_raw if output_raw else None
|
|
462
|
+
|
|
463
|
+
jd_raw = _prompt("Job description file (or Enter to skip)")
|
|
464
|
+
jd_path = jd_raw if jd_raw else None
|
|
465
|
+
|
|
466
|
+
compile_pdf = _prompt("Compile to PDF? (y/n)", "n").lower().startswith("y")
|
|
467
|
+
|
|
468
|
+
if not _check_provider_ready():
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
_echo("")
|
|
472
|
+
from receipts.cli import _run_rewrite_pipeline
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
_run_rewrite_pipeline(
|
|
476
|
+
resume_path,
|
|
477
|
+
source_path,
|
|
478
|
+
output_path,
|
|
479
|
+
0.5,
|
|
480
|
+
sections,
|
|
481
|
+
compile_pdf,
|
|
482
|
+
jd_path,
|
|
483
|
+
)
|
|
484
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
485
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# /score
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@_register("score", ["sc", "ats"], "Score resume against a JD (ATS scoring engine)")
|
|
494
|
+
def _cmd_score(args: str) -> None:
|
|
495
|
+
_echo("")
|
|
496
|
+
_echo(" ATS Score", bold=True)
|
|
497
|
+
_echo("")
|
|
498
|
+
|
|
499
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
500
|
+
if resume_path is None:
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
jd_path = _prompt_path("Job description file path")
|
|
504
|
+
if jd_path is None:
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
compare_raw = _prompt("Compare against another .tex? (path or Enter to skip)")
|
|
508
|
+
compare_path = compare_raw if compare_raw else None
|
|
509
|
+
|
|
510
|
+
show_stats = False
|
|
511
|
+
if compare_path:
|
|
512
|
+
show_stats = (
|
|
513
|
+
_prompt("Show statistical analysis? (y/n)", "y").lower().startswith("y")
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
_echo("")
|
|
517
|
+
from receipts.cli import _run_score
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
_run_score(resume_path, jd_path, compare_path, show_stats)
|
|
521
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
522
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# /dossier
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@_register("dossier", ["d", "prep"], "Generate the interview defense dossier")
|
|
531
|
+
def _cmd_dossier(args: str) -> None:
|
|
532
|
+
_echo("")
|
|
533
|
+
_echo(" Interview Defense Dossier", bold=True)
|
|
534
|
+
_echo("")
|
|
535
|
+
|
|
536
|
+
resume_path = _prompt_path("Resume path (.tex or .pdf)")
|
|
537
|
+
if resume_path is None:
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
source_spec = _prompt_source()
|
|
541
|
+
if source_spec is None:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
entries_raw = _prompt("Entry to cover (substring, or Enter for all)")
|
|
545
|
+
entries = [entries_raw] if entries_raw else None
|
|
546
|
+
|
|
547
|
+
output = _prompt("Output file", "dossier.md")
|
|
548
|
+
|
|
549
|
+
if not _check_provider_ready():
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
_echo("")
|
|
553
|
+
from receipts.cli import _run_dossier
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
_run_dossier(resume_path, source_spec, output, 0.5, None, entries)
|
|
557
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
558
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
# /readiness
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@_register("readiness", ["rd", "progress"], "Show mock-interview readiness trends")
|
|
567
|
+
def _cmd_readiness(args: str) -> None:
|
|
568
|
+
from receipts.cli import readiness as readiness_cmd
|
|
569
|
+
|
|
570
|
+
_echo("")
|
|
571
|
+
try:
|
|
572
|
+
readiness_cmd()
|
|
573
|
+
except (ReceiptsError, SystemExit) as exc:
|
|
574
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
575
|
+
_echo("")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ---------------------------------------------------------------------------
|
|
579
|
+
# /export
|
|
580
|
+
# ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@_register("export", ["e", "pdf"], "Export a .md or .tex file to PDF")
|
|
584
|
+
def _cmd_export(args: str) -> None:
|
|
585
|
+
_echo("")
|
|
586
|
+
_echo(" Export to PDF", bold=True)
|
|
587
|
+
_echo("")
|
|
588
|
+
|
|
589
|
+
if args:
|
|
590
|
+
source_path = Path(args)
|
|
591
|
+
if not source_path.is_file():
|
|
592
|
+
_echo(f" Not found: {source_path}", color=typer.colors.RED)
|
|
593
|
+
return
|
|
594
|
+
else:
|
|
595
|
+
source_path = _prompt_path("File to export (.md or .tex)")
|
|
596
|
+
if source_path is None:
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
output_raw = _prompt("Output PDF path (or Enter for default)")
|
|
600
|
+
|
|
601
|
+
from receipts.cli import export as export_cmd
|
|
602
|
+
|
|
603
|
+
_echo("")
|
|
604
|
+
try:
|
|
605
|
+
# Pass both params explicitly — typer's OptionInfo defaults only
|
|
606
|
+
# resolve when invoked through the CLI, not in a direct call.
|
|
607
|
+
export_cmd(str(source_path), output_raw or None)
|
|
608
|
+
except (ReceiptsError, SystemExit, typer.Exit):
|
|
609
|
+
pass # the command already printed the failure
|
|
610
|
+
_echo("")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------------------------------------
|
|
614
|
+
# /ledger
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@_register(
|
|
619
|
+
"ledger",
|
|
620
|
+
["l", "tokens", "spend"],
|
|
621
|
+
"Show the token/cost ledger (lifetime or last session)",
|
|
622
|
+
)
|
|
623
|
+
def _cmd_ledger(args: str) -> None:
|
|
624
|
+
_echo("")
|
|
625
|
+
scope = _prompt("Lifetime or last session? (l/s)", "l").lower()
|
|
626
|
+
|
|
627
|
+
from receipts.cli import ledger as ledger_cmd
|
|
628
|
+
|
|
629
|
+
_echo("")
|
|
630
|
+
try:
|
|
631
|
+
ledger_cmd(session=scope.startswith("s"))
|
|
632
|
+
except (ReceiptsError, SystemExit, typer.Exit):
|
|
633
|
+
pass
|
|
634
|
+
_echo("")
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# ---------------------------------------------------------------------------
|
|
638
|
+
# /dashboard
|
|
639
|
+
# ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@_register(
|
|
643
|
+
"dashboard",
|
|
644
|
+
["dash"],
|
|
645
|
+
"One-screen status TUI: provider, token spend, readiness",
|
|
646
|
+
)
|
|
647
|
+
def _cmd_dashboard(args: str) -> None:
|
|
648
|
+
from receipts.tui.hub import HubApp
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
HubApp(start="dashboard").run()
|
|
652
|
+
except (ReceiptsError, SystemExit, typer.Exit) as exc:
|
|
653
|
+
_echo(f" Error: {exc}", color=typer.colors.RED)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ---------------------------------------------------------------------------
|
|
657
|
+
# /quit
|
|
658
|
+
# ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@_register("quit", ["q", "exit", "bye"], "Exit the interactive session")
|
|
662
|
+
def _cmd_quit(args: str) -> None:
|
|
663
|
+
_echo("")
|
|
664
|
+
_echo(" Later. Go crush that interview.", color=typer.colors.GREEN)
|
|
665
|
+
_echo("")
|
|
666
|
+
raise SystemExit(0)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ---------------------------------------------------------------------------
|
|
670
|
+
# Fuzzy matching
|
|
671
|
+
# ---------------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _find_command(raw: str) -> tuple[Command | None, str]:
|
|
675
|
+
"""Match user input to a command. Returns (command, remaining_args)."""
|
|
676
|
+
raw = raw.strip()
|
|
677
|
+
if not raw:
|
|
678
|
+
return None, ""
|
|
679
|
+
|
|
680
|
+
if raw.startswith("/"):
|
|
681
|
+
raw = raw[1:]
|
|
682
|
+
|
|
683
|
+
parts = raw.split(None, 1)
|
|
684
|
+
cmd_str = parts[0].lower()
|
|
685
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
686
|
+
|
|
687
|
+
if cmd_str in _COMMANDS:
|
|
688
|
+
return _COMMANDS[cmd_str], args
|
|
689
|
+
|
|
690
|
+
matches = difflib.get_close_matches(
|
|
691
|
+
cmd_str, list(_COMMANDS.keys()), n=1, cutoff=0.5
|
|
692
|
+
)
|
|
693
|
+
if matches:
|
|
694
|
+
return _COMMANDS[matches[0]], args
|
|
695
|
+
|
|
696
|
+
return None, raw
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
# ---------------------------------------------------------------------------
|
|
700
|
+
# Main REPL loop
|
|
701
|
+
# ---------------------------------------------------------------------------
|
|
702
|
+
|
|
703
|
+
_BANNER = """
|
|
704
|
+
┌─────────────────────────────────────────────┐
|
|
705
|
+
│ RECEIPTS — Interactive Mode │
|
|
706
|
+
│ │
|
|
707
|
+
│ Type /help for commands, /quit to exit. │
|
|
708
|
+
│ Just start typing — I'll figure it out. │
|
|
709
|
+
└─────────────────────────────────────────────┘
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def run_repl() -> None:
|
|
714
|
+
"""Start the interactive REPL session."""
|
|
715
|
+
_echo(_BANNER, color=typer.colors.CYAN, bold=True)
|
|
716
|
+
|
|
717
|
+
while True:
|
|
718
|
+
try:
|
|
719
|
+
raw = input(" receipts> ").strip()
|
|
720
|
+
except (EOFError, KeyboardInterrupt):
|
|
721
|
+
_echo("")
|
|
722
|
+
_cmd_quit("")
|
|
723
|
+
break
|
|
724
|
+
|
|
725
|
+
if not raw:
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
cmd, args = _find_command(raw)
|
|
729
|
+
|
|
730
|
+
if cmd is None:
|
|
731
|
+
matches = difflib.get_close_matches(
|
|
732
|
+
raw.lstrip("/").split()[0].lower(),
|
|
733
|
+
list(_COMMANDS.keys()),
|
|
734
|
+
n=3,
|
|
735
|
+
cutoff=0.4,
|
|
736
|
+
)
|
|
737
|
+
if matches:
|
|
738
|
+
suggestions = ", ".join(f"/{m}" for m in matches)
|
|
739
|
+
_echo(
|
|
740
|
+
f" Unknown command. Did you mean: {suggestions}?",
|
|
741
|
+
color=typer.colors.YELLOW,
|
|
742
|
+
)
|
|
743
|
+
else:
|
|
744
|
+
_echo(
|
|
745
|
+
f" Unknown command: '{raw}'. Type /help for options.",
|
|
746
|
+
color=typer.colors.YELLOW,
|
|
747
|
+
)
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
try:
|
|
751
|
+
cmd.handler(args)
|
|
752
|
+
except SystemExit:
|
|
753
|
+
break
|
|
754
|
+
except Exception as exc:
|
|
755
|
+
_echo(f" Unexpected error: {exc}", color=typer.colors.RED)
|