stock-weekly-report 0.1.9 → 0.2.1

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/cli.py CHANGED
@@ -17,7 +17,6 @@ Usage:
17
17
 
18
18
  import os
19
19
  import re
20
- import shutil
21
20
  import smtplib
22
21
  import subprocess
23
22
  import sys
@@ -90,24 +89,6 @@ def _write_zprofile_var(var_name: str, value: str) -> None:
90
89
  zprofile.write_text(content, encoding="utf-8")
91
90
 
92
91
 
93
- def _detect_nlm() -> str:
94
- """Auto-detect the nlm binary path. Returns '' if not found."""
95
- # 1. Check PATH
96
- found = shutil.which("nlm")
97
- if found:
98
- return found
99
- # 2. Check common install locations
100
- candidates = [
101
- "~/.local/bin/nlm",
102
- "/usr/local/bin/nlm",
103
- "/opt/homebrew/bin/nlm",
104
- ]
105
- for candidate in candidates:
106
- expanded = Path(candidate).expanduser()
107
- if expanded.exists():
108
- return str(expanded)
109
- return ""
110
-
111
92
 
112
93
  def _install_cron_job(schedule: str) -> None:
113
94
  run_sh = PROJECT_ROOT / "run.sh"
@@ -151,12 +132,15 @@ def init(ctx):
151
132
 
152
133
  # 0. Project root (where pipeline.py and venv/ live)
153
134
  cwd = Path.cwd()
135
+ saved_root = cfg.get("project_root", "")
154
136
  if (cwd / "pipeline.py").exists():
155
137
  suggested_root = str(cwd)
138
+ elif saved_root and (Path(saved_root) / "pipeline.py").exists():
139
+ suggested_root = saved_root
156
140
  elif (PROJECT_ROOT / "pipeline.py").exists():
157
141
  suggested_root = str(PROJECT_ROOT)
158
142
  else:
159
- suggested_root = cfg.get("project_root", str(PROJECT_ROOT))
143
+ suggested_root = saved_root or ""
160
144
  project_root_input = click.prompt("Project root [required]", default=suggested_root)
161
145
  project_root_path = Path(project_root_input).expanduser().resolve()
162
146
  if not (project_root_path / "pipeline.py").exists():
@@ -168,21 +152,21 @@ def init(ctx):
168
152
  default_folder = cfg.get("parent_folder", str(Path.home() / "swr-data"))
169
153
  parent_folder = click.prompt("Data folder path [required]", default=default_folder)
170
154
 
171
- # 2. nlm binary path (optional only needed for NotebookLM upload stage)
172
- saved_nlm = cfg.get("nlm_path", "")
173
- default_nlm = (saved_nlm if saved_nlm and Path(saved_nlm).exists() else None) or _detect_nlm()
174
- click.echo(" (nlm is only needed for the NotebookLM upload stage; use --skip-upload to bypass)")
175
- nlm_path = click.prompt("nlm binary path [optional, leave blank to skip]", default=default_nlm or "")
176
- nlm_path = nlm_path.strip()
177
- if nlm_path:
178
- nlm_path_expanded = str(Path(nlm_path).expanduser())
179
- if Path(nlm_path_expanded).exists():
180
- click.echo(f" ✓ nlm found at {nlm_path_expanded}")
181
- else:
182
- click.echo(f" ! nlm not found at {nlm_path_expanded} fix this before using the upload stage")
155
+ # 2. nlm installed automatically by postinstall; check auth status
156
+ nlm_path_expanded = cfg.get("nlm_path", "")
157
+ if nlm_path_expanded and Path(nlm_path_expanded).exists():
158
+ click.echo(f" nlm found at {nlm_path_expanded}")
159
+ auth_ok = subprocess.run(
160
+ [nlm_path_expanded, "login", "--check"],
161
+ capture_output=True,
162
+ ).returncode == 0
163
+ if auth_ok:
164
+ click.echo(" ✓ nlm already authenticated")
165
+ elif click.confirm(" Log in to NotebookLM now? [optional — needed for upload stage]", default=True):
166
+ subprocess.run([nlm_path_expanded, "login"], check=False)
183
167
  else:
184
- nlm_path_expanded = ""
185
- click.echo(" Skipped use --skip-upload when running the pipeline.")
168
+ click.echo(" ! nlm not found — run: swr config set nlm_path /path/to/nlm")
169
+ click.echo(" (or reinstall: npm install -g stock-weekly-report)")
186
170
 
187
171
  # 3. SMTP password → ~/.zprofile (optional)
188
172
  existing_password = os.environ.get("EMAIL_SMTP_PASSWORD", "")
@@ -277,7 +261,6 @@ def init(ctx):
277
261
  cfg.update({
278
262
  "project_root": str(project_root_path),
279
263
  "parent_folder": parent_folder,
280
- "nlm_path": nlm_path_expanded,
281
264
  "email": email_cfg,
282
265
  "retention": {
283
266
  "audio_months": audio_months,
@@ -581,6 +564,26 @@ def config_set(ctx, key, value):
581
564
  click.echo(f"Set {key} = {obj[leaf]}")
582
565
 
583
566
 
567
+ # ─── nlm-login ────────────────────────────────────────────────────────────────
568
+
569
+ @main.command("nlm-login")
570
+ @click.pass_context
571
+ def nlm_login_cmd(ctx):
572
+ """Log in to NotebookLM (browser OAuth). Run once, or again if auth expires."""
573
+ cfg = _load_cfg(ctx.obj["config"])
574
+ nlm_path = cfg.get("nlm_path", "")
575
+ if not nlm_path or not Path(nlm_path).exists():
576
+ click.echo("Error: nlm not found. Set it with: swr config set nlm_path /path/to/nlm", err=True)
577
+ sys.exit(1)
578
+ auth_ok = subprocess.run([nlm_path, "login", "--check"], capture_output=True).returncode == 0
579
+ if auth_ok:
580
+ click.echo("Already authenticated. Use --force to re-login.")
581
+ if not click.confirm("Re-login anyway?", default=False):
582
+ return
583
+ result = subprocess.run([nlm_path, "login"], check=False)
584
+ sys.exit(result.returncode)
585
+
586
+
584
587
  # ─── mcp ──────────────────────────────────────────────────────────────────────
585
588
 
586
589
  @main.command("mcp")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stock-weekly-report",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Stock weekly podcast report pipeline — CLI and MCP server",
5
5
  "bin": {
6
6
  "swr": "bin/swr.js",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stock-weekly-report"
7
- version = "0.1.9"
7
+ version = "0.2.1"
8
8
  requires-python = ">=3.10"
9
9
  authors = [
10
10
  { name = "Chang Yu Chuan", email = "changyuchuanmicron@gmail.com" },
package/requirements.txt CHANGED
@@ -5,8 +5,10 @@ tqdm>=4.0
5
5
  faster-whisper>=1.0
6
6
  markdown>=3.0
7
7
  click>=8.0
8
- # notebooklm-mcp-cli is not on PyPI — install separately:
9
- # pip install notebooklm-mcp-cli (or use the pre-installed binary at nlm_path in config.yaml)
8
+ # notebooklm-mcp-cli requires Python >=3.11 — install into the CLI venv, not the pipeline venv:
9
+ # ~/.config/swr/venv/bin/pip install notebooklm-mcp-cli
10
+ # Then set nlm_path in config:
11
+ # swr config set nlm_path ~/.config/swr/venv/bin/nlm
10
12
 
11
13
  # CLI + MCP entry points require Python >=3.10; install via:
12
14
  # venv14/bin/pip install -e .
@@ -72,8 +72,44 @@ try {
72
72
 
73
73
  const pip = path.join(VENV, "bin", "pip");
74
74
  execFileSync(pip, ["install", "."], { stdio: "inherit", cwd: ROOT });
75
+ execFileSync(pip, ["install", "notebooklm-mcp-cli"], { stdio: "inherit", cwd: ROOT });
75
76
 
76
77
  fs.writeFileSync(VERSION_FILE, CURRENT_VER + "\n", "utf8");
78
+
79
+ // ── Set project_root in config if not already pointing to a valid location ─
80
+ const configPath = path.join(SWR_DIR, "config.yaml");
81
+ let needsProjectRoot = true;
82
+ if (fs.existsSync(configPath)) {
83
+ const content = fs.readFileSync(configPath, "utf8");
84
+ const match = content.match(/^project_root:\s*(.+)$/m);
85
+ if (match) {
86
+ const existing = match[1].trim().replace(/^['"]|['"]$/g, "");
87
+ if (fs.existsSync(path.join(existing, "pipeline.py"))) {
88
+ needsProjectRoot = false;
89
+ }
90
+ }
91
+ }
92
+ if (needsProjectRoot) {
93
+ execFileSync(SWR_BIN, ["config", "set", "project_root", ROOT], { stdio: "inherit" });
94
+ console.log(`swr: Set project_root to ${ROOT}`);
95
+ }
96
+
97
+ // ── Set nlm_path if not already pointing to a working binary ─────────────
98
+ const nlmBin = path.join(VENV, "bin", "nlm");
99
+ let needsNlmPath = true;
100
+ if (fs.existsSync(configPath)) {
101
+ const content = fs.readFileSync(configPath, "utf8");
102
+ const match = content.match(/^nlm_path:\s*(.+)$/m);
103
+ if (match) {
104
+ const existing = match[1].trim().replace(/^['"]|['"]$/g, "");
105
+ if (fs.existsSync(existing)) needsNlmPath = false;
106
+ }
107
+ }
108
+ if (needsNlmPath && fs.existsSync(nlmBin)) {
109
+ execFileSync(SWR_BIN, ["config", "set", "nlm_path", nlmBin], { stdio: "inherit" });
110
+ console.log(`swr: Set nlm_path to ${nlmBin}`);
111
+ }
112
+
77
113
  console.log(`\nswr: Setup complete (v${CURRENT_VER}). Run \`swr --help\` to get started.\n`);
78
114
  } catch (err) {
79
115
  console.error("\nswr postinstall failed:", err.message);