@tiens.nguyen/gonext-local-worker 1.0.162 → 1.0.165

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.
@@ -1156,17 +1156,39 @@ def _rag_workdir(source_key: str) -> str:
1156
1156
  return base
1157
1157
 
1158
1158
 
1159
- def _rag_download(url: str, dest_path: str = "") -> str:
1160
- """Download a public URL to a local path with SSRF + size guards. Returns the path."""
1159
+ def _rag_download(url: str, dest_path: str = "") -> tuple:
1160
+ """Download a public URL with SSRF + size guards IDEMPOTENT per URL. Every URL
1161
+ maps to a deterministic work dir (~/.gonext/rag-work/<urlHash>/); relative
1162
+ dest_paths are contained there (never the process cwd, which is the user's home),
1163
+ and a repeat call for an already-downloaded URL reuses the cached file instantly
1164
+ (follow-up questions re-run the pipeline cheaply). Returns (path, cached)."""
1161
1165
  import os
1162
1166
  src = _rag_gdrive_direct((url or "").strip())
1163
1167
  _rag_assert_safe_url(src)
1164
1168
  key = _rag_source_key(url)
1169
+ workdir = _rag_workdir(key)
1170
+ dest_path = os.path.expanduser((dest_path or "").strip())
1165
1171
  if not dest_path:
1166
1172
  name = os.path.basename(src.split("?")[0]) or "download.bin"
1167
1173
  if not name.lower().endswith(".zip") and "zip" in src.lower():
1168
1174
  name += ".zip"
1169
- dest_path = os.path.join(_rag_workdir(key), name)
1175
+ dest_path = os.path.join(workdir, name)
1176
+ elif not os.path.isabs(dest_path):
1177
+ # Contain relative paths in the per-URL work dir — models pass bare names like
1178
+ # "project.zip" which would otherwise land in the worker's cwd (the home dir).
1179
+ dest_path = os.path.join(workdir, dest_path)
1180
+ # Cache hit 1: the exact target already exists.
1181
+ if os.path.isfile(dest_path) and os.path.getsize(dest_path) > 0:
1182
+ return dest_path, True
1183
+ # Cache hit 2: this URL was downloaded before under a different name — the work dir
1184
+ # is per-URL, so any archive already in it IS this URL's file.
1185
+ try:
1186
+ for fn in sorted(os.listdir(workdir)):
1187
+ p = os.path.join(workdir, fn)
1188
+ if os.path.isfile(p) and fn.lower().endswith(".zip") and os.path.getsize(p) > 0:
1189
+ return p, True
1190
+ except OSError:
1191
+ pass
1170
1192
  os.makedirs(os.path.dirname(dest_path) or ".", exist_ok=True)
1171
1193
  req = urllib.request.Request(src, headers={"User-Agent": "gonext-rag/1.0"})
1172
1194
  total = 0
@@ -1184,15 +1206,48 @@ def _rag_download(url: str, dest_path: str = "") -> str:
1184
1206
  f"Download exceeds the {_RAG_MAX_DOWNLOAD_BYTES // (1024*1024)} MB cap."
1185
1207
  )
1186
1208
  out.write(block)
1187
- return dest_path
1209
+ return dest_path, False
1210
+
1211
+
1212
+ def _rag_resolve_zip(zip_path: str) -> str:
1213
+ """Resolve a (possibly relative) zip path. Bare names the model echoes from earlier
1214
+ turns are searched in the rag work dirs so a fresh process still finds them."""
1215
+ import glob as _glob
1216
+ import os
1217
+ zp = os.path.expanduser((zip_path or "").strip())
1218
+ if os.path.isfile(zp):
1219
+ return zp
1220
+ if not os.path.isabs(zp):
1221
+ hits = _glob.glob(os.path.join(
1222
+ os.path.expanduser("~"), ".gonext", "rag-work", "*", os.path.basename(zp)))
1223
+ if hits:
1224
+ return hits[0]
1225
+ return zp
1188
1226
 
1189
1227
 
1190
1228
  def _rag_unzip(zip_path: str, dest_dir: str = "") -> tuple:
1191
- """Extract a local zip with Zip-Slip + zip-bomb guards. Returns (dir, file_list)."""
1229
+ """Extract a local zip with Zip-Slip + zip-bomb guards — IDEMPOTENT. If the target
1230
+ dir already has extracted files, reuse it instead of re-extracting. Relative
1231
+ dest_dirs are placed next to the zip (inside the per-URL work dir), never the cwd.
1232
+ Returns (dir, file_list, cached)."""
1192
1233
  import os
1193
1234
  import zipfile
1235
+ zip_path = _rag_resolve_zip(zip_path)
1236
+ dest_dir = os.path.expanduser((dest_dir or "").strip())
1194
1237
  if not dest_dir:
1195
1238
  dest_dir = os.path.splitext(zip_path)[0] + "_unzipped"
1239
+ elif not os.path.isabs(dest_dir):
1240
+ dest_dir = os.path.join(os.path.dirname(zip_path) or ".", dest_dir)
1241
+ # Cache hit: already extracted for this zip → return the existing listing.
1242
+ if os.path.isdir(dest_dir):
1243
+ existing = []
1244
+ for dirpath, _dirnames, filenames in os.walk(dest_dir):
1245
+ for fn in filenames:
1246
+ existing.append(os.path.relpath(os.path.join(dirpath, fn), dest_dir))
1247
+ if len(existing) > _RAG_MAX_FILES:
1248
+ break
1249
+ if existing:
1250
+ return dest_dir, sorted(existing), True
1196
1251
  os.makedirs(dest_dir, exist_ok=True)
1197
1252
  dest_root = os.path.realpath(dest_dir)
1198
1253
  written = 0
@@ -1216,7 +1271,7 @@ def _rag_unzip(zip_path: str, dest_dir: str = "") -> tuple:
1216
1271
  dst.write(src.read())
1217
1272
  written += 1
1218
1273
  names.append(info.filename)
1219
- return dest_dir, names
1274
+ return dest_dir, names, False
1220
1275
 
1221
1276
 
1222
1277
  def _rag_iter_text_files(root: str):
@@ -1776,8 +1831,11 @@ def run_agent_chat(cfg):
1776
1831
  "THEN list_dir(dir) to find the README, THEN read_text_file(that README) THEN answer from it. "
1777
1832
  "This is the FASTEST path for a summary — do NOT index unless deep Q&A across many files is needed.\n"
1778
1833
  "- user wants deep Q&A across the WHOLE project -> download_file THEN unzip_file THEN "
1779
- "rag_index(dir, source_url=url) THEN rag_search(source_url=url, query=<question>) THEN answer. "
1780
- "For FOLLOW-UPs on the SAME url, just rag_search (do NOT re-download). "
1834
+ "rag_index(dir, source_url=url) THEN rag_search(source_url=url, query=<question>) THEN answer.\n"
1835
+ "- FOLLOW-UP question about a ZIP url from EARLIER in the conversation -> try "
1836
+ "rag_search(source_url=url, query=<question>) FIRST. If it says no knowledge base exists, "
1837
+ "run download_file(url) + unzip_file(zip) again — both are CACHED per url and return "
1838
+ "instantly — then list_dir/read_text_file or rag_index as needed. "
1781
1839
  "If the user adds information, rag_add(source_url=url, text=...).\n"
1782
1840
  ) if _RAG_AVAILABLE else ""
1783
1841
  _pdf_read_tool_line = (
@@ -2718,10 +2776,14 @@ def run_agent_chat(cfg):
2718
2776
  url: public http(s) URL of the file to download.
2719
2777
  dest_path: optional local save path; leave empty for an automatic work-dir path.
2720
2778
  """
2721
- _emit({"type": "step", "text": f"Downloading → {url[:80]}"})
2722
2779
  try:
2723
2780
  import os
2724
- path = _rag_download(url, (dest_path or "").strip())
2781
+ path, cached = _rag_download(url, (dest_path or "").strip())
2782
+ if cached:
2783
+ _emit({"type": "step", "text": "Already downloaded — reusing cached file"})
2784
+ return (f"Already downloaded earlier — reusing {path} "
2785
+ f"({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}').")
2786
+ _emit({"type": "step", "text": f"Downloading → {url[:80]}"})
2725
2787
  return f"Downloaded to {path} ({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}') if it is a zip."
2726
2788
  except Exception as e: # noqa: BLE001
2727
2789
  return f"Error: download failed: {type(e).__name__}: {e}"
@@ -2734,11 +2796,15 @@ def run_agent_chat(cfg):
2734
2796
  zip_path: local path to the .zip (from download_file).
2735
2797
  dest_dir: optional target directory; leave empty for an automatic one.
2736
2798
  """
2737
- _emit({"type": "step", "text": "Unzipping…"})
2738
2799
  try:
2739
- d, names = _rag_unzip((zip_path or "").strip(), (dest_dir or "").strip())
2800
+ d, names, cached = _rag_unzip((zip_path or "").strip(), (dest_dir or "").strip())
2740
2801
  preview = ", ".join(names[:15])
2741
2802
  more = f" …(+{len(names) - 15} more)" if len(names) > 15 else ""
2803
+ if cached:
2804
+ _emit({"type": "step", "text": "Already unzipped — reusing extracted files"})
2805
+ return (f"Already unzipped earlier — {len(names)} files at {d}. "
2806
+ f"Files: {preview}{more}. Use list_dir/read_text_file or rag_index on it.")
2807
+ _emit({"type": "step", "text": "Unzipping…"})
2742
2808
  return f"Unzipped {len(names)} files to {d}. Files: {preview}{more}. Next: rag_index(path='{d}', source_url=<the original url>)."
2743
2809
  except Exception as e: # noqa: BLE001
2744
2810
  return f"Error: unzip failed: {type(e).__name__}: {e}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.162",
3
+ "version": "1.0.165",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",