openvisio-agent 0.11.1 → 0.12.0

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/README.md CHANGED
@@ -62,12 +62,15 @@ Routine messages, triage, introductions, catch-up checks, and ticket movement us
62
62
  openvisio-agent watch --name ada # run in this terminal
63
63
  openvisio-agent watch --name ada --install # run in the background, start at login
64
64
  openvisio-agent watch --name ada --workdir ~/repo # allow REAL work on a git branch
65
+ openvisio-agent stop --name ada # stop service + every ada watcher
65
66
  ```
66
67
 
67
68
  With `--workdir`, the agent gets file + Bash tools scoped to that repo and works on a branch. Guardrails are built in: it never pushes or merges, and destructive shell (`git push`, `rm`, `sudo`, `curl`, publish, PR-merge, …) is denied.
68
69
 
69
70
  `--install` sets up a background service (launchd on macOS, systemd `--user` on Linux) that runs `watch` and restarts on login. Logs go to `~/.openvisio/<agent>.log` (macOS) or `journalctl --user -u openvisio-<agent>` (Linux).
70
71
 
72
+ Do not chase auto-changing watcher PIDs. `openvisio-agent stop --name <agent>` unloads the named background service first, stops every remaining watcher for that exact agent, and clears its stale lock. Running `watch --install` also performs this cleanup before replacing the service.
73
+
71
74
  ## Security
72
75
 
73
76
  - **No opaque script.** You run a named, versioned npm package you can read here and on [npmjs.com](https://www.npmjs.com/package/openvisio-agent).
package/bin/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
14
  import { fileURLToPath } from 'node:url'
15
15
  import { dirname, join } from 'node:path'
16
16
  import { parseFlags, slugify, stripSlash, exchangeToken, ensureClaude, ensureCodex, writeJson, mcpConfigPath, configPath, chmodSafe, onPath, OV_DIR, fail, ok, info } from '../src/lib.mjs'
17
- import { runWatch, installService } from '../src/watch.mjs'
17
+ import { runWatch, installService, stopWatchers } from '../src/watch.mjs'
18
18
 
19
19
  const HERE = dirname(fileURLToPath(import.meta.url))
20
20
  const VERSION = (() => { try { return JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8')).version } catch { return '0.0.0' } })()
@@ -27,6 +27,7 @@ Usage:
27
27
  openvisio-agent connect <ovs_code> --host <url> [--name "<agent>"] [--mcp-url <url>] [--agent claude|codex|opencode]
28
28
  openvisio-agent connect --backend <url> --key <api-key> --id <identifier> [--name "<agent>"] [--ws <wss-url>] [--mcp-url <url>] [--agent claude|codex|opencode]
29
29
  openvisio-agent watch --name <agent> [--install] [--workspace <dir>] [--chat-only] [--model <m>] [--chat-model <m>] [--debug]
30
+ openvisio-agent stop --name <agent>
30
31
  openvisio-agent --help | --version
31
32
 
32
33
  connect
@@ -72,6 +73,11 @@ watch
72
73
  setup needed. Use --workspace <dir> to relocate it (e.g. an existing clones folder),
73
74
  --chat-only to disable code work, and --install to run in the background on login.
74
75
 
76
+ stop
77
+ Stops the named agent's launchd/systemd service first, then terminates every
78
+ remaining watcher with that exact --name and clears its stale lock. Use this
79
+ instead of killing changing PIDs: openvisio-agent stop --name Alex
80
+
75
81
  Docs: https://www.npmjs.com/package/openvisio-agent`
76
82
 
77
83
  // Register the `openvisio-team` MCP at USER (global) scope so it's available in
@@ -280,6 +286,11 @@ async function main() {
280
286
  const rest = parseFlags(argv.slice(1))
281
287
  if (cmd === 'connect') return runConnect(rest)
282
288
  if (cmd === 'watch') return runWatch(rest)
289
+ if (cmd === 'stop') {
290
+ const name = String(rest.flags.name || rest.positional[0] || '')
291
+ if (!name) fail('Missing agent name.\n Usage: openvisio-agent stop --name <agent>')
292
+ return stopWatchers({ slug: slugify(name) })
293
+ }
283
294
  fail(`Unknown command "${cmd}".\n\n${HELP}`)
284
295
  }
285
296
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Connect Claude Code, Codex, or OpenCode to an OpenVisio team — MCP tools + optional autonomy — in one command.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -229,18 +229,13 @@ export async function runWatch({ flags }) {
229
229
  const lock = acquireSingleInstance(slug || 'openvisio')
230
230
  if (lock.conflict) {
231
231
  const watcherName = slug || 'openvisio'
232
- const stop = process.platform === 'darwin'
233
- ? `launchctl unload ~/Library/LaunchAgents/io.openvisio.${watcherName}.plist`
234
- : process.platform === 'win32'
235
- ? 'Stop the existing openvisio-agent process in Task Manager.'
236
- : `systemctl --user stop openvisio-${watcherName}.service`
237
232
  const logs = process.platform === 'darwin'
238
233
  ? `tail -f ~/.openvisio/${watcherName}.log`
239
234
  : `journalctl --user -u openvisio-${watcherName} -f`
240
235
  fail(`Another openvisio-agent watcher for "${watcherName}" is already running (pid ${lock.conflict}).\n` +
241
236
  ` Two watchers for the same agent BOTH reply to every mention — that is what causes duplicate/contradicting messages.\n` +
242
237
  ` This is usually the auto-restarting background service; killing its PID only makes it restart.\n` +
243
- ` To run in this terminal instead, stop the service first:\n ${stop}\n` +
238
+ ` Stop the service and every watcher for this agent with:\n openvisio-agent stop --name ${watcherName}\n` +
244
239
  ` Or keep the service and inspect its log:\n ${logs}\n` +
245
240
  ` Refusing to start a second watcher.`)
246
241
  }
@@ -871,7 +866,57 @@ function loop({ host, key, slug, claude, agent, mcpConfig, mcpUrl, workdir, mode
871
866
  }
872
867
 
873
868
  // ── background service install (launchd / systemd) ───────────────────────────
869
+ /** Stop the auto-restarting service first, then every orphaned watcher whose
870
+ * --name resolves to this exact slug. Never use a broad pkill pattern. */
871
+ export function stopWatchers({ slug, quiet = false }) {
872
+ const key = slugify(slug || 'openvisio')
873
+ let serviceStopped = false
874
+
875
+ if (process.platform === 'darwin') {
876
+ const plist = join(homedir(), 'Library', 'LaunchAgents', `io.openvisio.${key}.plist`)
877
+ if (existsSync(plist)) {
878
+ const r = spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' })
879
+ serviceStopped = r.status === 0
880
+ }
881
+ } else if (process.platform !== 'win32') {
882
+ const r = spawnSync('systemctl', ['--user', 'stop', `openvisio-${key}.service`], { stdio: 'ignore' })
883
+ serviceStopped = r.status === 0
884
+ }
885
+
886
+ const watcherPids = () => {
887
+ if (process.platform === 'win32') return []
888
+ const r = spawnSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' })
889
+ if (r.status !== 0) return []
890
+ const out = []
891
+ for (const line of String(r.stdout || '').split(/\r?\n/)) {
892
+ const m = /^\s*(\d+)\s+(.+)$/.exec(line)
893
+ if (!m) continue
894
+ const pid = Number(m[1]); const command = m[2]
895
+ if (pid === process.pid || !/\bopenvisio-agent\b/.test(command) || !/\bwatch\b/.test(command)) continue
896
+ const name = /(?:^|\s)--name(?:=|\s+)["']?([^"'\s]+)/.exec(command)?.[1]
897
+ if (name && slugify(name) === key) out.push(pid)
898
+ }
899
+ return out
900
+ }
901
+
902
+ const found = watcherPids()
903
+ for (const pid of found) { try { process.kill(pid, 'SIGTERM') } catch { /* already stopped */ } }
904
+ if (found.length) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 600)
905
+ const stubborn = watcherPids()
906
+ for (const pid of stubborn) { try { process.kill(pid, 'SIGKILL') } catch { /* already stopped */ } }
907
+ if (stubborn.length) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 150)
908
+
909
+ const remaining = watcherPids()
910
+ if (remaining.length) fail(`Could not stop watcher${remaining.length === 1 ? '' : 's'} for "${key}": ${remaining.join(', ')}`)
911
+ try { unlinkSync(join(OV_DIR, `watch-${key}.lock`)) } catch { /* absent */ }
912
+ if (!quiet) ok(`Stopped ${found.length} watcher process${found.length === 1 ? '' : 'es'} for "${key}"${serviceStopped ? ' and disabled its background service' : ''}.`)
913
+ return { stopped: found.length, serviceStopped }
914
+ }
915
+
874
916
  export function installService({ slug, workdir }) {
917
+ // Replacing a service must also remove manually-started watchers. Otherwise the
918
+ // new KeepAlive process repeatedly spawns, sees their lock, exits, and respawns.
919
+ stopWatchers({ slug, quiet: true })
875
920
  const binPath = onPath('openvisio-agent')
876
921
  if (!binPath || binPath.includes('/_npx/')) {
877
922
  info('Installing openvisio-agent globally so the background service has a stable path…')