forgexa-cli 1.0.4__tar.gz → 1.1.4__tar.gz

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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.0.4
3
+ Version: 1.1.4
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
- Author-email: Shinetech <dev@shinetechsoftware.com>
5
+ Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://forgexa.net
8
8
  Project-URL: Documentation, https://docs.forgexa.net
@@ -228,3 +228,73 @@ password = pypi-AgEIcH...
228
228
  EOF
229
229
  chmod 600 ~/.pypirc
230
230
  ```
231
+
232
+ ## Local Development & Debugging
233
+
234
+ ### Editable Install
235
+
236
+ ```bash
237
+ cd cli
238
+ pip install -e .
239
+ ```
240
+
241
+ This installs the `forgexa` command pointing to your local source. Changes take effect immediately.
242
+
243
+ ### Testing Against Local Server
244
+
245
+ ```bash
246
+ # Point CLI to local backend
247
+ export FORGEXA_SERVER_URL=http://localhost:8000
248
+
249
+ # Login
250
+ forgexa login
251
+
252
+ # Test commands
253
+ forgexa workspace list
254
+ forgexa daemon start
255
+ ```
256
+
257
+ ### Testing Against Remote/LAN Server
258
+
259
+ ```bash
260
+ export FORGEXA_SERVER_URL=http://192.168.0.100:8000
261
+ forgexa login
262
+ forgexa daemon start
263
+ ```
264
+
265
+ ### Debugging the Daemon
266
+
267
+ ```bash
268
+ # Run in foreground to see all logs
269
+ forgexa daemon start
270
+
271
+ # Check which agents are discovered
272
+ forgexa runtimes list
273
+
274
+ # Verbose logging (if supported)
275
+ DAEMON_LOG_LEVEL=DEBUG forgexa daemon start
276
+ ```
277
+
278
+ ### Project Structure
279
+
280
+ ```
281
+ cli/
282
+ ├── forgexa_cli/
283
+ │ ├── __init__.py # Version constant
284
+ │ ├── main.py # CLI entry point (argparse)
285
+ │ ├── daemon.py # Daemon implementation
286
+ │ └── py.typed # PEP 561 marker
287
+ ├── scripts/
288
+ │ ├── bump-version.sh # Version management
289
+ │ ├── publish.sh # PyPI publishing
290
+ │ └── sync-daemon.sh # Sync daemon code from backend
291
+ ├── pyproject.toml # Package metadata
292
+ └── README.md # This file
293
+ ```
294
+
295
+ ### Design Principles
296
+
297
+ - **Zero external dependencies** — uses only Python stdlib (urllib, json, subprocess)
298
+ - **Lightweight** — installs in seconds, no compilation needed
299
+ - **Cross-platform** — works on Linux, macOS, Windows
300
+ - **Standalone daemon** — discovers local AI agents without server-side configuration
@@ -199,3 +199,73 @@ password = pypi-AgEIcH...
199
199
  EOF
200
200
  chmod 600 ~/.pypirc
201
201
  ```
202
+
203
+ ## Local Development & Debugging
204
+
205
+ ### Editable Install
206
+
207
+ ```bash
208
+ cd cli
209
+ pip install -e .
210
+ ```
211
+
212
+ This installs the `forgexa` command pointing to your local source. Changes take effect immediately.
213
+
214
+ ### Testing Against Local Server
215
+
216
+ ```bash
217
+ # Point CLI to local backend
218
+ export FORGEXA_SERVER_URL=http://localhost:8000
219
+
220
+ # Login
221
+ forgexa login
222
+
223
+ # Test commands
224
+ forgexa workspace list
225
+ forgexa daemon start
226
+ ```
227
+
228
+ ### Testing Against Remote/LAN Server
229
+
230
+ ```bash
231
+ export FORGEXA_SERVER_URL=http://192.168.0.100:8000
232
+ forgexa login
233
+ forgexa daemon start
234
+ ```
235
+
236
+ ### Debugging the Daemon
237
+
238
+ ```bash
239
+ # Run in foreground to see all logs
240
+ forgexa daemon start
241
+
242
+ # Check which agents are discovered
243
+ forgexa runtimes list
244
+
245
+ # Verbose logging (if supported)
246
+ DAEMON_LOG_LEVEL=DEBUG forgexa daemon start
247
+ ```
248
+
249
+ ### Project Structure
250
+
251
+ ```
252
+ cli/
253
+ ├── forgexa_cli/
254
+ │ ├── __init__.py # Version constant
255
+ │ ├── main.py # CLI entry point (argparse)
256
+ │ ├── daemon.py # Daemon implementation
257
+ │ └── py.typed # PEP 561 marker
258
+ ├── scripts/
259
+ │ ├── bump-version.sh # Version management
260
+ │ ├── publish.sh # PyPI publishing
261
+ │ └── sync-daemon.sh # Sync daemon code from backend
262
+ ├── pyproject.toml # Package metadata
263
+ └── README.md # This file
264
+ ```
265
+
266
+ ### Design Principles
267
+
268
+ - **Zero external dependencies** — uses only Python stdlib (urllib, json, subprocess)
269
+ - **Lightweight** — installs in seconds, no compilation needed
270
+ - **Cross-platform** — works on Linux, macOS, Windows
271
+ - **Standalone daemon** — discovers local AI agents without server-side configuration
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.0.4"
2
+ __version__ = "1.1.4"
@@ -0,0 +1,2 @@
1
+ # Auto-generated by publish.sh — do not edit manually.
2
+ BUILD_SERVER_URL = "https://api.forgexa.net"
@@ -46,6 +46,10 @@ try:
46
46
  except (ImportError, ModuleNotFoundError):
47
47
  # Running standalone (e.g., from desktop app bundle or forgexa-cli).
48
48
  # Provide a minimal settings object reading from environment variables.
49
+ # Production default for end-user installs; server-side deployments
50
+ # override via .env or systemd Environment= directives.
51
+ _STANDALONE_DEFAULT_URL = "https://api.forgexa.net"
52
+
49
53
  class _StandaloneSettings:
50
54
  """Minimal settings shim for standalone daemon execution."""
51
55
 
@@ -55,7 +59,7 @@ except (ImportError, ModuleNotFoundError):
55
59
 
56
60
  @property
57
61
  def DAEMON_SERVER_URL(self) -> str:
58
- return os.environ.get("DAEMON_SERVER_URL", "http://localhost:8000")
62
+ return os.environ.get("DAEMON_SERVER_URL", _STANDALONE_DEFAULT_URL)
59
63
 
60
64
  @property
61
65
  def DAEMON_SERVER_URLS(self) -> str:
@@ -105,11 +109,29 @@ except (ImportError, ModuleNotFoundError):
105
109
 
106
110
  settings = _StandaloneSettings() # type: ignore[assignment]
107
111
 
112
+ # ── Logging — self-managed file handler ────────────────────────────────
113
+ # The daemon configures its own FileHandler so logs are written to
114
+ # ~/.forgexa/daemon/daemon.log regardless of how the daemon was launched
115
+ # (Makefile, Tauri desktop app, `forgexa daemon start --detach`, etc.).
116
+ # A StreamHandler is added only when stderr is a TTY (foreground mode)
117
+ # so logs are visible on the terminal without duplication.
118
+ _log_dir = Path.home() / ".forgexa" / "daemon"
119
+ _log_dir.mkdir(parents=True, exist_ok=True)
120
+ DAEMON_LOG_PATH = _log_dir / "daemon.log"
121
+
122
+ _log_handlers: list[logging.Handler] = [
123
+ logging.FileHandler(DAEMON_LOG_PATH, mode="a", encoding="utf-8"),
124
+ ]
125
+ if sys.stderr.isatty():
126
+ _log_handlers.append(logging.StreamHandler(sys.stderr))
127
+
108
128
  logging.basicConfig(
109
129
  level=logging.INFO,
110
130
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
131
+ handlers=_log_handlers,
111
132
  )
112
133
  logger = logging.getLogger("daemon")
134
+ logger.info("Daemon log: %s", DAEMON_LOG_PATH)
113
135
 
114
136
 
115
137
  # ── Hardware ID — stable cross-IP machine fingerprint ──
@@ -313,21 +335,91 @@ class AgentDiscovery:
313
335
  },
314
336
  }
315
337
 
338
+ @staticmethod
339
+ def _expand_path():
340
+ """Augment PATH with well-known agent CLI directories.
341
+
342
+ GUI apps (macOS, Windows) and systemd services often have a
343
+ minimal PATH that does not include user-installed CLI tools.
344
+ We probe common installation directories for each platform so
345
+ ``shutil.which`` can locate agent binaries.
346
+ """
347
+ home = Path.home()
348
+ is_win = sys.platform == "win32"
349
+ is_mac = sys.platform == "darwin"
350
+
351
+ extra_dirs: list[Path] = []
352
+
353
+ if is_win:
354
+ # Windows: npm global, AppData installs, scoop, cargo, bun
355
+ appdata = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
356
+ localappdata = Path(os.environ.get("LOCALAPPDATA", home / "AppData" / "Local"))
357
+ extra_dirs += [
358
+ appdata / "npm", # npm -g installs
359
+ localappdata / "Programs" / "Python" / "Scripts",
360
+ home / ".opencode" / "bin",
361
+ home / ".cargo" / "bin",
362
+ home / ".bun" / "bin",
363
+ home / "scoop" / "shims", # scoop package manager
364
+ ]
365
+ # nvm-windows stores versions differently
366
+ nvm_home = os.environ.get("NVM_HOME", "")
367
+ if nvm_home:
368
+ nvm_path = Path(nvm_home)
369
+ nvm_symlink = os.environ.get("NVM_SYMLINK", "")
370
+ if nvm_symlink:
371
+ extra_dirs.append(Path(nvm_symlink))
372
+ elif nvm_path.is_dir():
373
+ versions = sorted(nvm_path.glob("v*/"), reverse=True)
374
+ if versions:
375
+ extra_dirs.append(versions[0])
376
+ else:
377
+ # macOS + Linux shared paths
378
+ extra_dirs += [
379
+ home / ".local" / "bin", # pip / pipx installs, Claude Code
380
+ home / ".opencode" / "bin", # opencode
381
+ home / ".cargo" / "bin", # Rust / cargo installs
382
+ home / ".bun" / "bin", # bun
383
+ home / ".volta" / "bin", # volta (node version manager)
384
+ ]
385
+ if is_mac:
386
+ # Homebrew Intel + Apple Silicon
387
+ extra_dirs += [
388
+ Path("/usr/local/bin"),
389
+ Path("/opt/homebrew/bin"),
390
+ ]
391
+ # nvm (macOS + Linux)
392
+ nvm_dir = os.environ.get("NVM_DIR", str(home / ".nvm"))
393
+ nvm_path = Path(nvm_dir)
394
+ if nvm_path.is_dir():
395
+ versions = sorted(nvm_path.glob("versions/node/*/bin"), reverse=True)
396
+ if versions:
397
+ extra_dirs.append(versions[0])
398
+
399
+ current = os.environ.get("PATH", "")
400
+ current_set = set(current.split(os.pathsep))
401
+ additions = [str(d) for d in extra_dirs if d.is_dir() and str(d) not in current_set]
402
+ if additions:
403
+ os.environ["PATH"] = os.pathsep.join(additions) + os.pathsep + current
404
+ logger.debug("Expanded PATH with: %s", ", ".join(additions))
405
+
316
406
  async def discover(self) -> list[DiscoveredAgent]:
407
+ self._expand_path()
317
408
  available = []
318
409
  for agent_id, spec in self.AGENT_REGISTRY.items():
319
410
  custom_path = os.environ.get(spec.get("env_path_override", ""))
320
411
  cmd = custom_path or spec["commands"][0]
321
- if shutil.which(cmd):
412
+ resolved = shutil.which(cmd)
413
+ if resolved:
322
414
  version = await self._get_version(spec["detect"])
323
415
  available.append(DiscoveredAgent(
324
416
  agent_id=agent_id,
325
- command=cmd,
417
+ command=resolved,
326
418
  version=version,
327
419
  invoke_modes=spec["invoke_modes"],
328
420
  compatibility_level=spec["compatibility_level"],
329
421
  ))
330
- logger.info("Discovered agent: %s v%s (%s)", agent_id, version, cmd)
422
+ logger.info("Discovered agent: %s v%s (%s)", agent_id, version, resolved)
331
423
  return available
332
424
 
333
425
  async def _get_version(self, detect_cmd: str) -> str:
@@ -5,7 +5,7 @@ A lightweight, standalone CLI that communicates with the Forgexa server via REST
5
5
  Zero external dependencies — uses only Python stdlib.
6
6
 
7
7
  Configuration:
8
- FORGEXA_SERVER_URL Server URL (default: http://localhost:8000)
8
+ FORGEXA_SERVER_URL Server URL (default: https://api.forgexa.net)
9
9
  FORGEXA_TOKEN Bearer token (obtain via `forgexa login`)
10
10
 
11
11
  Usage:
@@ -30,8 +30,20 @@ from pathlib import Path
30
30
  # ── HTTP helpers (stdlib only) ──
31
31
 
32
32
 
33
+ # Default server URL — resolved in priority order:
34
+ # 1. FORGEXA_SERVER_URL environment variable (runtime override)
35
+ # 2. _build_config.py — generated by publish.sh at wheel-build time
36
+ # 3. Hardcoded fallback — https://api.forgexa.net
37
+ #
38
+ # For local development use: FORGEXA_SERVER_URL=http://localhost:8000 forgexa ...
39
+ try:
40
+ from forgexa_cli._build_config import BUILD_SERVER_URL as _DEFAULT_SERVER_URL
41
+ except ImportError:
42
+ _DEFAULT_SERVER_URL = "https://api.forgexa.net"
43
+
44
+
33
45
  def _api_url() -> str:
34
- return os.environ.get("FORGEXA_SERVER_URL", "http://localhost:8000")
46
+ return os.environ.get("FORGEXA_SERVER_URL", _DEFAULT_SERVER_URL)
35
47
 
36
48
 
37
49
  def _token() -> str | None:
@@ -218,20 +230,26 @@ def cmd_daemon_start(args: argparse.Namespace) -> None:
218
230
  os.environ.setdefault("DAEMON_API_TOKEN", token)
219
231
 
220
232
  if getattr(args, "detach", False):
221
- # Background mode: spawn forgexa-daemon as detached subprocess
233
+ # Background mode: spawn forgexa-daemon as detached subprocess.
234
+ # Redirect stderr to the canonical log file so logs are not lost.
222
235
  import subprocess as sp
223
236
 
237
+ log_path = Path.home() / ".forgexa" / "daemon" / "daemon.log"
238
+ log_path.parent.mkdir(parents=True, exist_ok=True)
239
+
224
240
  cmd = [sys.executable, "-m", "forgexa_cli.daemon"]
225
- proc = sp.Popen(
226
- cmd,
227
- stdout=sp.DEVNULL,
228
- stderr=sp.DEVNULL,
229
- start_new_session=True,
230
- )
241
+ with open(log_path, "a", encoding="utf-8") as log_fh:
242
+ proc = sp.Popen(
243
+ cmd,
244
+ stdout=sp.DEVNULL,
245
+ stderr=log_fh,
246
+ start_new_session=True,
247
+ )
231
248
  pid_file = Path.home() / ".forgexa-daemon.pid"
232
249
  pid_file.write_text(str(proc.pid))
233
250
  print(f"Daemon started in background (PID {proc.pid})")
234
251
  print(f"Server: {server_url}")
252
+ print(f"Logs: {log_path}")
235
253
  print(f"Stop with: forgexa daemon stop")
236
254
  else:
237
255
  # Foreground mode: run daemon directly
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.0.4
3
+ Version: 1.1.4
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
- Author-email: Shinetech <dev@shinetechsoftware.com>
5
+ Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://forgexa.net
8
8
  Project-URL: Documentation, https://docs.forgexa.net
@@ -228,3 +228,73 @@ password = pypi-AgEIcH...
228
228
  EOF
229
229
  chmod 600 ~/.pypirc
230
230
  ```
231
+
232
+ ## Local Development & Debugging
233
+
234
+ ### Editable Install
235
+
236
+ ```bash
237
+ cd cli
238
+ pip install -e .
239
+ ```
240
+
241
+ This installs the `forgexa` command pointing to your local source. Changes take effect immediately.
242
+
243
+ ### Testing Against Local Server
244
+
245
+ ```bash
246
+ # Point CLI to local backend
247
+ export FORGEXA_SERVER_URL=http://localhost:8000
248
+
249
+ # Login
250
+ forgexa login
251
+
252
+ # Test commands
253
+ forgexa workspace list
254
+ forgexa daemon start
255
+ ```
256
+
257
+ ### Testing Against Remote/LAN Server
258
+
259
+ ```bash
260
+ export FORGEXA_SERVER_URL=http://192.168.0.100:8000
261
+ forgexa login
262
+ forgexa daemon start
263
+ ```
264
+
265
+ ### Debugging the Daemon
266
+
267
+ ```bash
268
+ # Run in foreground to see all logs
269
+ forgexa daemon start
270
+
271
+ # Check which agents are discovered
272
+ forgexa runtimes list
273
+
274
+ # Verbose logging (if supported)
275
+ DAEMON_LOG_LEVEL=DEBUG forgexa daemon start
276
+ ```
277
+
278
+ ### Project Structure
279
+
280
+ ```
281
+ cli/
282
+ ├── forgexa_cli/
283
+ │ ├── __init__.py # Version constant
284
+ │ ├── main.py # CLI entry point (argparse)
285
+ │ ├── daemon.py # Daemon implementation
286
+ │ └── py.typed # PEP 561 marker
287
+ ├── scripts/
288
+ │ ├── bump-version.sh # Version management
289
+ │ ├── publish.sh # PyPI publishing
290
+ │ └── sync-daemon.sh # Sync daemon code from backend
291
+ ├── pyproject.toml # Package metadata
292
+ └── README.md # This file
293
+ ```
294
+
295
+ ### Design Principles
296
+
297
+ - **Zero external dependencies** — uses only Python stdlib (urllib, json, subprocess)
298
+ - **Lightweight** — installs in seconds, no compilation needed
299
+ - **Cross-platform** — works on Linux, macOS, Windows
300
+ - **Standalone daemon** — discovers local AI agents without server-side configuration
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  forgexa_cli/__init__.py
4
+ forgexa_cli/_build_config.py
4
5
  forgexa_cli/daemon.py
5
6
  forgexa_cli/main.py
6
7
  forgexa_cli/py.typed
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.0.4"
3
+ version = "1.1.4"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = { text = "MIT" }
7
7
  authors = [
8
- { name = "Shinetech", email = "dev@shinetechsoftware.com" },
8
+ { name = "Jason Sun", email = "dev.winds@gmail.com" },
9
9
  ]
10
10
  readme = "README.md"
11
11
  keywords = ["forgexa", "ai", "software-factory", "cli", "devops", "agent"]
File without changes