@the-sanjeevkumar/nova-agent 1.0.0 → 1.0.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/npm/nova.js CHANGED
@@ -19,6 +19,14 @@ const APP_ROOT = resolve(__dirname, '..');
19
19
  const NOVA_HOME = process.env.NOVA_HOME || join(homedir(), '.nova');
20
20
  const VENV_DIR = join(NOVA_HOME, 'venv');
21
21
  const IS_WIN = platform() === 'win32';
22
+ // Termux reports process.platform === 'android'; $PREFIX distinguishes it from
23
+ // other Android Pythons. The Python floor below must match pyproject.toml:
24
+ // every runtime dependency is pinned with `python_version >= '3.14'`, so a
25
+ // venv built on older Python installs the package with no dependencies and
26
+ // `nova` dies on the first import (seen on Termux, which ships 3.12/3.13).
27
+ const IS_TERMUX = platform() === 'android' || (process.env.PREFIX || '').includes('com.termux');
28
+ const MIN_PY_MAJOR = 3;
29
+ const MIN_PY_MINOR = 14;
22
30
 
23
31
  const VERSION = readVersion();
24
32
 
@@ -30,6 +38,16 @@ function readVersion() {
30
38
  }
31
39
  }
32
40
 
41
+ function pythonTooOldError() {
42
+ console.error(`Nova Agent requires Python ${MIN_PY_MAJOR}.${MIN_PY_MINOR} or newer.`);
43
+ if (IS_TERMUX) {
44
+ console.error('Termux: run `pkg update && pkg install python`, then re-run `nova`.');
45
+ } else {
46
+ console.error('Install Python from https://www.python.org/downloads/ and re-run `nova`.');
47
+ }
48
+ process.exit(1);
49
+ }
50
+
33
51
  function findPython() {
34
52
  const candidates = IS_WIN ? ['python', 'python3', 'py'] : ['python3', 'python'];
35
53
  for (const c of candidates) {
@@ -39,11 +57,11 @@ function findPython() {
39
57
  const m = (r.stdout + r.stderr).match(/Python (\d+)\.(\d+)\.(\d+)/);
40
58
  if (!m) continue;
41
59
  const [major, minor] = [parseInt(m[1]), parseInt(m[2])];
42
- if (major > 3 || (major === 3 && minor >= 11)) {
60
+ if (major > MIN_PY_MAJOR || (major === MIN_PY_MAJOR && minor >= MIN_PY_MINOR)) {
43
61
  return c === 'py' ? { cmd: 'py', prefix: ['-3'] } : { cmd: c, prefix: [] };
44
62
  }
45
63
  }
46
- return null;
64
+ pythonTooOldError();
47
65
  }
48
66
 
49
67
  function venvPython() {
@@ -67,11 +85,17 @@ function markerPath() {
67
85
  return join(VENV_DIR, '.nova-version');
68
86
  }
69
87
 
70
- function needsProvision() {
88
+ function provisionMarker(py) {
89
+ // Two lines: launcher VERSION, then the venv's Python major.minor. A marker
90
+ // without the Python line (pre-1.0.2 installs) forces re-provision, so venvs
91
+ // built under older Python are not silently reused.
92
+ return `${VERSION}\n${py ? `${py.major}.${py.minor}` : 'unknown'}\n`;
93
+ }
94
+
95
+ function needsProvision(py) {
71
96
  if (!existsSync(venvPython()) || !existsSync(venvNova())) return true;
72
97
  try {
73
- const installed = readFileSync(markerPath(), 'utf8').trim();
74
- return installed !== VERSION;
98
+ return readFileSync(markerPath(), 'utf8') !== provisionMarker(py);
75
99
  } catch {
76
100
  return true;
77
101
  }
@@ -79,11 +103,6 @@ function needsProvision() {
79
103
 
80
104
  function provision() {
81
105
  const py = findPython();
82
- if (!py) {
83
- console.error('Nova Agent requires Python 3.11 or newer.');
84
- console.error('Install Python from https://www.python.org/downloads/ and re-run `nova`.');
85
- process.exit(1);
86
- }
87
106
 
88
107
  mkdirSync(NOVA_HOME, { recursive: true });
89
108
 
@@ -105,15 +124,18 @@ function provision() {
105
124
  const install = runSync(venvPython(), ['-m', 'pip', 'install', '--quiet', APP_ROOT]);
106
125
  if (install !== 0) {
107
126
  console.error('Nova Agent installation failed.');
127
+ console.error('pip printed the failure above. If you are on Termux, PyPI wheels for');
128
+ console.error('pydantic-core/psutil do not target Android/Bionic libc; use the Nova');
129
+ console.error('Termux deb package (or a PM wheelhouse) instead of the npm launcher.');
108
130
  process.exit(install);
109
131
  }
110
132
 
111
- writeFileSync(markerPath(), VERSION + '\n');
133
+ writeFileSync(markerPath(), provisionMarker(py));
112
134
  console.log('✅ Nova Agent ready.\n');
113
135
  }
114
136
 
115
137
  function main() {
116
- if (needsProvision()) {
138
+ if (needsProvision(findPython())) {
117
139
  provision();
118
140
  }
119
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-sanjeevkumar/nova-agent",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Nova Agent - a universal AI agent you can run from any terminal. Installs a self-contained Python environment on first run.",
5
5
  "license": "MIT",
6
6
  "author": "Sanjeev Kumar",
@@ -139,12 +139,24 @@ def record_activation_inputs(stamps: Path, mtimes: dict[str, int], project_root:
139
139
  stamps.mkdir(parents=True, exist_ok=True)
140
140
  (stamps / ".project-root").write_text(str(Path(project_root).resolve()), encoding="utf-8")
141
141
  if test_environment:
142
- (stamps / ".test-environment").touch()
143
- for name, mtime in mtimes.items():
144
- stamp = stamps / name
145
- stamp.parent.mkdir(parents=True, exist_ok=True)
146
- stamp.touch()
147
- os.utime(stamp, ns=(mtime, mtime))
142
+ # The reader (scripts/_activation.sh::nova_activation_current) compares the
143
+ # file's CONTENT against the flag it composes (default "--test-environment").
144
+ # An empty touch() read as "" and falsified every post-sync activation check,
145
+ # so run-in-nova-env always reported "did not produce a current environment".
146
+ (stamps / ".test-environment").write_text("--test-environment\n", encoding="utf-8")
147
+ manifest = stamps / "inputs"
148
+ with open(manifest, "wb") as f:
149
+ # The same reader's mtime loop ports "<input=stamp>\0" pairs from this
150
+ # manifest and judges staleness pairwise (`-nt`/`-ot`); the per-input
151
+ # stamp files alone give it nothing to read, and the check then fails
152
+ # on the missing file (bash reports line 88: inputs: No such file).
153
+ for name, mtime in mtimes.items():
154
+ stamp = stamps / name
155
+ stamp.parent.mkdir(parents=True, exist_ok=True)
156
+ stamp.touch()
157
+ os.utime(stamp, ns=(mtime, mtime))
158
+ input_path = Path(project_root) / name
159
+ f.write(f"{input_path}={stamp}\0".encode())
148
160
 
149
161
 
150
162
  def _payload_manifest(root: Path) -> tuple[dict, Path] | None:
package/pyproject.toml CHANGED
@@ -67,7 +67,15 @@ dependencies = [
67
67
  # arrangement dated to the package's uv exclude-newer quarantine, which
68
68
  # has long expired). PM's doc-extract extra repairs lean/broken installs
69
69
  # from this same dependency declaration.
70
- "firecrawl-anydoc==0.2.4; python_version >= '3.14'",
70
+ # The `sys_platform != 'android'` clause (same shape as psutil's split
71
+ # and nemo-relay's wheel-target guard) keeps the Android/Bionic python
72
+ # out: PyPI ships no bionic wheels for anydoc, so a faithful marker-less
73
+ # pin sends the phone's pip into an sdist build that bricks the whole
74
+ # npm/curl setup run (#Termux: "failed to build firecrawl-anydoc").
75
+ # `tools/read_extract.py` degrades to the typed NeedsOcrError teaching
76
+ # error without it; the Termux deb wheelhouse builds natives itself and
77
+ # is unaffected by marker evaluation on the build container.
78
+ "firecrawl-anydoc==0.2.4; python_version >= '3.14' and sys_platform != 'android'",
71
79
  # Bumped from 2.12.5 to 2.13.4 to pull in pydantic-core 2.46.4.
72
80
  # pydantic-core 2.41.5 (pulled by 2.12.5) segfaults when the OpenAI SDK's
73
81
  # Responses API resource is exercised from a non-main thread, which is the
@@ -373,7 +381,7 @@ google-chat = [
373
381
  # Document extraction (read_file anydoc converter self-heal) — core already
374
382
  # bundles firecrawl-anydoc==0.2.4; the extra exists so a lean/broken install
375
383
  # can re-sync exactly the pin from core.
376
- doc-extract = ["firecrawl-anydoc==0.2.4"]
384
+ doc-extract = ["firecrawl-anydoc==0.2.4; python_version >= '3.14' and sys_platform != 'android'"]
377
385
  # Office/PDF authoring libraries for skills/productivity/{docx,xlsx,powerpoint,pdf}.
378
386
  # Baked into the Docker image (Dockerfile `--extra documents`), whose sealed
379
387
  # venv has no pip and whose root fs resets on restart; ~25 MB there. Not in
@@ -611,6 +619,13 @@ matrix = "sys_platform == 'linux'"
611
619
  google-chat = "platform_machine != 'ARM64' or sys_platform != 'win32'"
612
620
  # Match the engine dependency markers so PM refuses an unsupported install.
613
621
  wake-openwakeword = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')"
622
+ # wake-openwakeword = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')"
623
+ # doc-extract: marker must equal its dependency marker — the gates contract
624
+ # (tests/pm/test_extras.py::test_declared_extra_gates_match_dependency_selection)
625
+ # compares gate vs dep-marker admission per platform, and ensure_import must
626
+ # refuse cleanly on the bionic python instead of resolving the extra to an
627
+ # empty set in a loop.
628
+ doc-extract = "python_version >= '3.14' and sys_platform != 'android'"
614
629
  # faster-whisper's closure is prebuilt-only: ctranslate2 has no win_arm64 wheel and onnxruntime
615
630
  # none for darwin-x64. Its ANCHOR is faster_whisper, so an unsupported machine could install the
616
631
  # extra, rebuild the whole dependency environment and still fail the anchor on every retry.
package/uv.lock CHANGED
@@ -3686,7 +3686,7 @@ dependencies = [
3686
3686
  { name = "cryptography" },
3687
3687
  { name = "fastapi" },
3688
3688
  { name = "fire" },
3689
- { name = "firecrawl-anydoc" },
3689
+ { name = "firecrawl-anydoc", marker = "sys_platform != 'android'" },
3690
3690
  { name = "httptools" },
3691
3691
  { name = "httpx", extra = ["socks"] },
3692
3692
  { name = "jinja2" },
@@ -3782,7 +3782,7 @@ discord = [
3782
3782
  { name = "discord-py", extra = ["voice"] },
3783
3783
  ]
3784
3784
  doc-extract = [
3785
- { name = "firecrawl-anydoc" },
3785
+ { name = "firecrawl-anydoc", marker = "sys_platform != 'android'" },
3786
3786
  ]
3787
3787
  documents = [
3788
3788
  { name = "openpyxl" },
@@ -4027,8 +4027,8 @@ requires-dist = [
4027
4027
  { name = "faster-whisper", marker = "(platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'stt-whisper') or (platform_machine != 'ARM64' and sys_platform == 'win32' and extra == 'stt-whisper') or (sys_platform != 'android' and sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'stt-whisper')", specifier = "==1.2.1" },
4028
4028
  { name = "faster-whisper", marker = "(platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'voice') or (platform_machine != 'ARM64' and sys_platform == 'win32' and extra == 'voice') or (sys_platform != 'android' and sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'voice')", specifier = "==1.2.1" },
4029
4029
  { name = "fire", marker = "python_full_version >= '3.14'", specifier = "==0.7.1" },
4030
- { name = "firecrawl-anydoc", marker = "python_full_version >= '3.14'", specifier = "==0.2.4" },
4031
- { name = "firecrawl-anydoc", marker = "extra == 'doc-extract'", specifier = "==0.2.4" },
4030
+ { name = "firecrawl-anydoc", marker = "python_full_version >= '3.14' and sys_platform != 'android'", specifier = "==0.2.4" },
4031
+ { name = "firecrawl-anydoc", marker = "python_full_version >= '3.14' and sys_platform != 'android' and extra == 'doc-extract'", specifier = "==0.2.4" },
4032
4032
  { name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = "==4.53.2" },
4033
4033
  { name = "google-api-python-client", marker = "extra == 'google'", specifier = "==2.201.0" },
4034
4034
  { name = "google-auth", marker = "extra == 'google'", specifier = "==2.62.0" },