@misterhuydo/sentinel 1.0.43 → 1.0.45

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/.cairn/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-03-21T22:25:43.489Z
1
+ 2026-03-22T05:33:05.646Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-21T22:26:00.108Z",
3
- "checkpoint_at": "2026-03-21T22:26:00.109Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T05:36:04.794Z",
3
+ "checkpoint_at": "2026-03-22T05:36:04.795Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -69,6 +69,9 @@ What you can do (tools available):
69
69
  11. list_errors — List recent errors from the state store, optionally filtered by repo or source.
70
70
  e.g. "show all errors today", "what errors hit elprint this week?"
71
71
 
72
+ 12. pull_repo — Run git pull on one or all managed repos.
73
+ e.g. "pull changes for sentinel-1881", "git pull all repos", "update the code"
74
+
72
75
  Tone: direct, professional, like a senior engineer who owns the system.
73
76
  Don't pad responses. Don't say "Great question!" or "Certainly!".
74
77
  If you don't know something, use a tool to find out before saying you don't know.
@@ -229,9 +232,84 @@ _TOOLS = [
229
232
  },
230
233
  },
231
234
  },
235
+ {
236
+ "name": "pull_repo",
237
+ "description": (
238
+ "Run git pull on one or all managed repos to fetch latest changes from GitHub. "
239
+ "Use for: 'pull changes', 'git pull', 'update repo X', 'fetch latest code'."
240
+ ),
241
+ "input_schema": {
242
+ "type": "object",
243
+ "properties": {
244
+ "repo": {
245
+ "type": "string",
246
+ "description": "Repo name to pull (omit to pull all configured repos)",
247
+ },
248
+ },
249
+ },
250
+ },
251
+ {
252
+ "name": "pull_config",
253
+ "description": (
254
+ "Run git pull on one or all Sentinel project config directories. "
255
+ "Projects are matched by short name ('1881', 'elprint') or full dir name ('sentinel-1881'). "
256
+ "Use for: 'pull config for 1881', 'update sentinel config', 'pull all configs'."
257
+ ),
258
+ "input_schema": {
259
+ "type": "object",
260
+ "properties": {
261
+ "project": {
262
+ "type": "string",
263
+ "description": "Project short name or dir name to pull (omit for all projects)",
264
+ },
265
+ },
266
+ },
267
+ },
232
268
  ]
233
269
 
234
270
 
271
+ # ── Workspace helpers ─────────────────────────────────────────────────────────
272
+
273
+ def _workspace_dir() -> Path:
274
+ return Path(".").resolve().parent
275
+
276
+ def _short_name(dir_name: str) -> str:
277
+ """'sentinel-1881' → '1881', 'sentinel-elprint' → 'elprint', others unchanged."""
278
+ if dir_name.startswith("sentinel-"):
279
+ return dir_name[len("sentinel-"):]
280
+ return dir_name
281
+
282
+ def _find_project_dirs(target: str = "") -> list[Path]:
283
+ """Return project dirs matching target (short or full name), or all if target empty."""
284
+ workspace = _workspace_dir()
285
+ results = []
286
+ try:
287
+ for d in sorted(workspace.iterdir()):
288
+ if not d.is_dir() or d.name in ("code", ".git"):
289
+ continue
290
+ if not (d / "config").exists():
291
+ continue
292
+ if target:
293
+ if target.lower() not in d.name.lower() and target.lower() not in _short_name(d.name).lower():
294
+ continue
295
+ results.append(d)
296
+ except Exception:
297
+ pass
298
+ return results
299
+
300
+ def _git_pull(path: Path) -> dict:
301
+ try:
302
+ r = subprocess.run(
303
+ ["git", "pull", "--rebase", "origin"],
304
+ cwd=str(path), capture_output=True, text=True, timeout=60,
305
+ )
306
+ last = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else "already up to date"
307
+ return {"status": "ok" if r.returncode == 0 else "error",
308
+ "detail": last if r.returncode == 0 else r.stderr.strip()}
309
+ except Exception as e:
310
+ return {"status": "error", "detail": str(e)}
311
+
312
+
235
313
  # ── Tool execution ────────────────────────────────────────────────────────────
236
314
 
237
315
  def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
@@ -320,27 +398,11 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
320
398
  return json.dumps({"status": "resumed"})
321
399
 
322
400
  if name == "list_projects":
323
- # Repos this instance manages
324
- my_repos = [
325
- {
326
- "repo": r.repo_name,
327
- "url": r.repo_url,
328
- "branch": r.branch,
329
- "auto_publish": r.auto_publish,
330
- }
331
- for r in cfg_loader.repos.values()
332
- ]
333
- # Scan workspace for sibling project instances
334
- workspace = Path(".").resolve().parent
335
- other_projects = []
336
- try:
337
- for d in sorted(workspace.iterdir()):
338
- if not d.is_dir() or d.name in ("code", ".git"):
339
- continue
340
- repo_cfg_dir = d / "config" / "repo-configs"
341
- if not repo_cfg_dir.exists():
342
- continue
343
- repos_in_project = []
401
+ projects = []
402
+ for d in _find_project_dirs():
403
+ repo_cfg_dir = d / "config" / "repo-configs"
404
+ repos_in_project = []
405
+ if repo_cfg_dir.exists():
344
406
  for p in sorted(repo_cfg_dir.glob("*.properties")):
345
407
  if p.name.startswith("_"):
346
408
  continue
@@ -349,25 +411,15 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
349
411
  if line.startswith("REPO_URL"):
350
412
  repo_url = line.split("=", 1)[-1].strip()
351
413
  break
352
- if repo_url:
353
- repos_in_project.append({"repo": p.stem, "url": repo_url})
354
- if repos_in_project:
355
- pid_file = d / "sentinel.pid"
356
- running = pid_file.exists()
357
- other_projects.append({
358
- "project": d.name,
359
- "running": running,
360
- "repos": repos_in_project,
361
- })
362
- except Exception as e:
363
- logger.warning("list_projects workspace scan failed: %s", e)
364
- return json.dumps({
365
- "this_instance": {
366
- "project": Path(".").resolve().name,
367
- "repos": my_repos,
368
- },
369
- "workspace_projects": other_projects,
370
- })
414
+ repos_in_project.append({"repo": p.stem, "url": repo_url})
415
+ projects.append({
416
+ "project": _short_name(d.name),
417
+ "dir": d.name,
418
+ "running": (d / "sentinel.pid").exists(),
419
+ "this": d.resolve() == Path(".").resolve(),
420
+ "repos": repos_in_project,
421
+ })
422
+ return json.dumps({"projects": projects})
371
423
 
372
424
  if name == "search_logs":
373
425
  query = inputs.get("query", "")
@@ -439,6 +491,42 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
439
491
  pass
440
492
  return json.dumps({"sentinel_commits": results})
441
493
 
494
+ if name == "pull_repo":
495
+ target = inputs.get("repo", "").lower()
496
+ results = []
497
+ for repo_name, repo in cfg_loader.repos.items():
498
+ if target and target not in repo_name.lower():
499
+ continue
500
+ local = Path(repo.local_path)
501
+ if not local.exists():
502
+ results.append({"repo": repo_name, "status": "error", "detail": "local path not found"})
503
+ continue
504
+ try:
505
+ r = subprocess.run(
506
+ ["git", "pull", "--rebase", "origin", repo.branch],
507
+ cwd=str(local), capture_output=True, text=True, timeout=60,
508
+ )
509
+ last_line = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else "already up to date"
510
+ if r.returncode == 0:
511
+ results.append({"repo": repo_name, "status": "ok", "detail": last_line})
512
+ else:
513
+ results.append({"repo": repo_name, "status": "error", "detail": r.stderr.strip()})
514
+ except Exception as e:
515
+ results.append({"repo": repo_name, "status": "error", "detail": str(e)})
516
+ return json.dumps({"results": results})
517
+
518
+ if name == "pull_config":
519
+ target = inputs.get("project", "")
520
+ dirs = _find_project_dirs(target)
521
+ if not dirs:
522
+ return json.dumps({"error": f"No project found matching '{target}'"})
523
+ results = []
524
+ for d in dirs:
525
+ res = _git_pull(d)
526
+ results.append({"project": _short_name(d.name), "dir": d.name, **res})
527
+ logger.info("Boss: pull_config %s → %s", d.name, res["status"])
528
+ return json.dumps({"results": results})
529
+
442
530
  return json.dumps({"error": f"unknown tool: {name}"})
443
531
 
444
532