openvisio-agent 0.3.5 → 0.3.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/watch.mjs +28 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Connect your coding agent (Claude Code) to an OpenVisio team — MCP tools + optional autonomy — in one command. No shell scripts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -11,15 +11,13 @@ import { join, dirname } from 'node:path'
11
11
  import { OV_DIR, readConfig, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
12
12
  import { connectAgentWs, assertWebSocket } from './ws.mjs'
13
13
 
14
- // Shared preamble the #1 cause of slow replies was the model shelling around to
15
- // "find credentials". The openvisio-team MCP tools are authenticated by the
16
- // connection itself, so this hammers that shut.
17
- const AUTH_NOTE = 'IMPORTANT: your openvisio-team (mcp__openvisio-team__*) tools are ALREADY authenticated through this connection. You do NOT need — and must NEVER look for — any API key, credential, identifier, token, or config file. NEVER run Bash/shell/grep/cat/find or read your memory to locate credentials or tools; just CALL the openvisio-team tools directly. If one returns unauthorized, say so briefly in one line — do not try to fix it by hunting for keys. Act immediately with the fewest tool calls.'
18
-
19
- const CYCLE = 'Run one OpenVisio autonomy cycle. ' + AUTH_NOTE
20
- const CYCLE_FAST = AUTH_NOTE + ' New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message. Then call poll_inbox once and also handle any .followUps (thread replies you are part of, even without an @mention) — ignore .tasks/.claimable. Post AT MOST ONE reply per channel: if the same person sent several nudges, answer them together in ONE post. HONESTY: your only tools are the openvisio-team chat/ticket toolsyou cannot write code or touch other tools; if asked for work you have no tool for, say so plainly in one short message (or offer to file a ticket). Never invent progress. Reply in 1-3 sentences, no summary. Then stop.'
21
- const CODE_FULL = 'Run one OpenVisio autonomy cycle. ' + AUTH_NOTE + ' Call get_marching_orders and poll_inbox. You have file + Bash tools and a git repo at your working directory FOR CODE WORK ONLY. For assigned tickets or mentions asking for real code work: FIRST "git checkout -B agent/work", make the changes with Read/Edit/Write, run tests if present, then "git add -A && git commit -m ...". NEVER git push, merge, or touch main. Then comment_ticket with a short summary + the branch name, and post a brief channel reply. Do NOT use Bash for anything other than git/tests/build — never to find credentials. If you truly cannot (missing repo/specs), say so in one message — never fabricate.'
22
- const CODE_FAST = AUTH_NOTE + ' New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message — do this before any Bash. Then poll_inbox and handle .followUps (thread replies you are part of, even without an @mention), AT MOST ONE reply per channel. Only if a message asks for real CODE work in your git repo, do it on a branch (checkout -B agent/work, edit, commit locally — never push/merge) and reply with the branch name. Bash is for git/tests ONLY, never for credentials. 1-3 sentences, no summary. Then stop.'
14
+ // Behaviour prompts. The openvisio-team MCP bridge requires the agent's
15
+ // credentials as ARGUMENTS on every tool call — those are injected at runtime by
16
+ // loopBackendWs (see credNote), NOT baked in here, so nothing needs hunting.
17
+ const CYCLE = 'Run one OpenVisio autonomy cycle.'
18
+ const CYCLE_FAST = 'New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message. Then call poll_inbox and handle any .followUps (thread replies you are part of, even without an @mention) — ignore .tasks/.claimable. Post AT MOST ONE reply per channel: if the same person sent several nudges, answer them together in ONE post. HONESTY: your only tools are the openvisio-team chat/ticket tools — you cannot write code or touch other tools; if asked for work you have no tool for, say so plainly in one short message (or offer to file a ticket). Never invent progress. Reply in 1-3 sentences, no summary. Then stop.'
19
+ const CODE_FULL = 'Run one OpenVisio autonomy cycle. Call get_marching_orders and poll_inbox. You have file + Bash tools and a git repo at your working directory FOR CODE WORK ONLY. For assigned tickets or mentions asking for real code work: FIRST "git checkout -B agent/work", make the changes with Read/Edit/Write, run tests if present, then "git add -A && git commit -m ...". NEVER git push, merge, or touch main. Then comment_ticket with a short summary + the branch name, and post a brief channel reply. Do NOT use Bash to find credentials — they are given to you. If you truly cannot (missing repo/specs), say so in one message — never fabricate.'
20
+ const CODE_FAST = 'New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message — do this before any Bash. Then poll_inbox and handle .followUps (thread replies you are part of, even without an @mention), AT MOST ONE reply per channel. Only if a message asks for real CODE work in your git repo, do it on a branch (checkout -B agent/work, edit, commit locally never push/merge) and reply with the branch name. Bash is for git/tests ONLY, never for credentials. 1-3 sentences, no summary. Then stop.'
23
21
 
24
22
  const CODE_TOOLS = ['Read', 'Grep', 'Glob', 'Edit', 'Write', 'MultiEdit', 'TodoWrite', 'Bash', 'mcp__openvisio-team__*']
25
23
  const DENY_TOOLS = ['Bash(git push:*)', 'Bash(git reset --hard:*)', 'Bash(git clean:*)', 'Bash(rm:*)', 'Bash(sudo:*)', 'Bash(chmod:*)', 'Bash(curl:*)', 'Bash(wget:*)', 'Bash(npm publish:*)', 'Bash(pnpm publish:*)', 'Bash(gh pr merge:*)', 'Bash(gh repo:*)']
@@ -160,20 +158,16 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
160
158
  let busy = false
161
159
  let queued = null // 'full' | 'fast' — a cycle requested while one was running
162
160
  let handle = null
163
- let lastActivityAt = Date.now()
164
161
  // Context lines from the events themselves (the WS payload already carries the
165
162
  // channel + message / task), so the agent acts on THEM directly instead of
166
163
  // hoping poll_inbox re-surfaces the same item. Accumulated across coalesced
167
164
  // events and drained into the next cycle's prompt.
168
165
  const pending = []
169
166
 
170
- // Typing indicator pulse `channel:typing` for the channel we're working on so
171
- // the reply feels immediate while the cycle runs. Cleared when the queue drains.
172
- let typingChan = null
173
- let typingTimer = null
174
- const pulseTyping = () => { if (typingChan != null && handle) handle.sendTyping(typingChan) }
175
- const startTyping = (cid) => { if (cid == null) return; typingChan = cid; pulseTyping(); if (!typingTimer) typingTimer = setInterval(pulseTyping, 3000) }
176
- const stopTyping = () => { typingChan = null; if (typingTimer) { clearInterval(typingTimer); typingTimer = null } }
167
+ // The Mastra bridge authenticates per-CALL, not per-connection: every
168
+ // openvisio-team tool needs agent_identifier + agent_api_key as arguments.
169
+ // Hand them to the model up front so it never shells around hunting for them.
170
+ const credNote = `AUTH: the openvisio-team (mcp__openvisio-team__*) tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call (post_message, poll_inbox, react_message, comment_ticket, …). They are given to you right here: do NOT search for them, do NOT run Bash/shell/grep/cat/find, do NOT read memory — just call the tools with these exact values.`
177
171
 
178
172
  async function drain(kind, context) {
179
173
  if (context) pending.push(context)
@@ -181,14 +175,13 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
181
175
  busy = true
182
176
  const ctx = pending.splice(0) // take everything accumulated so far
183
177
  const base = kind === 'full' ? fullPrompt : fastPrompt
184
- const prompt = ctx.length ? ctx.join('\n') + '\n\n' + base : base
178
+ const prompt = credNote + '\n\n' + (ctx.length ? ctx.join('\n') + '\n\n' : '') + base
185
179
  log('running ' + kind + ' cycle…' + (ctx.length ? ' (' + ctx.length + ' event' + (ctx.length === 1 ? '' : 's') + ')' : ''))
186
180
  try {
187
181
  await runCycle(prompt)
188
182
  } finally {
189
183
  busy = false
190
184
  if (queued || pending.length) { const next = queued || 'fast'; queued = null; void drain(next) }
191
- else stopTyping()
192
185
  }
193
186
  }
194
187
 
@@ -202,7 +195,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
202
195
 
203
196
  function onEvent(k, d) {
204
197
  const raw = d && typeof d === 'object' ? d : {}
205
- lastActivityAt = Date.now()
206
198
  if (k === 'task:assigned') {
207
199
  const t = raw.task || {}
208
200
  log('task:assigned ' + (t.id != null ? '#' + t.id + ' “' + (t.title || '') + '”' : ''))
@@ -213,35 +205,36 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
213
205
  const msg = raw.message && typeof raw.message === 'object' ? raw.message : {}
214
206
  const text = String(msg.content || msg.body || msg.text || '').replace(/\s+/g, ' ').slice(0, 600)
215
207
  const who = senderName(msg)
216
- log('agent:mention in channel ' + (cid != null ? cid : '?'))
217
- startTyping(cid) // show the agent as typing immediately, before the cycle finishes
208
+ // Reply IN THE SAME THREAD: parent is the thread root (the message's own id
209
+ // for a top-level mention, or its parent when the mention is itself a reply).
210
+ const mid = msg.id != null ? msg.id : (msg.message_id != null ? msg.message_id : null)
211
+ const parent = msg.parent_id != null ? msg.parent_id : (msg.parentId != null ? msg.parentId : null)
212
+ const threadRoot = parent != null ? parent : mid
213
+ log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
218
214
  const ctx = cid != null
219
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by ${who}` : ''}: "${text}". Your FIRST action must be mcp__openvisio-team__post_message with channel_id ${cid} and a 1-3 sentence reply you already have the message here, so do NOT poll_inbox to find it and do NOT run any Bash/shell to look for credentials or tools (they are already authenticated). After posting, you MAY poll_inbox once for other .followUps.`
215
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". Reply with mcp__openvisio-team__post_message as your FIRST action arguments: channel_id ${cid}${threadRoot != null ? `, parent_id ${threadRoot} (reply IN THAT THREAD, do not start a new top-level message)` : ''}, plus agent_identifier + agent_api_key from the AUTH line above, and a 1-3 sentence reply.${who ? ` To @mention them back, write their EXACT full name "@${who}" (a mention only links when the name matches exactly "@${who.split(' ')[0]}" alone will NOT).` : ''} You already have the message here do NOT poll_inbox to find it.`
220
216
  : undefined
221
217
  void drain('fast', ctx)
218
+ } else if (k === 'error') {
219
+ // Surface the backend's rejection detail instead of a bare "event error".
220
+ log('error event: ' + JSON.stringify(raw).slice(0, 220))
222
221
  } else {
223
222
  log('event ' + k)
224
223
  }
225
224
  }
226
225
 
227
- // Follow-up sweep: thread replies you are part of get NO @mention (no WS event
228
- // fires), so poll the inbox on a slow cadence to catch .followUps — but ONLY
229
- // within a short window after real activity, so an idle agent costs nothing.
230
- const FOLLOWUP_MS = 60_000
231
- const ACTIVE_WINDOW_MS = 5 * 60_000
232
- const sweep = setInterval(() => {
233
- if (busy) return
234
- if (Date.now() - lastActivityAt > ACTIVE_WINDOW_MS) return // idle → don't spend a cycle
235
- void drain('fast', 'Follow-up check: call poll_inbox and reply to any NEW .followUps (thread replies you are part of, even with no @mention) or unanswered .mentions. If there is nothing new, do nothing and stop — do NOT post.')
236
- }, FOLLOWUP_MS)
237
-
226
+ // NO polling the WebSocket is the only trigger (task:assigned / agent:mention).
227
+ // A thread reply that @mentions the agent fires agent:mention and is handled
228
+ // in-thread above; genuinely un-mentioned thread activity has no WS event, so
229
+ // it's a backend concern (dispatch a thread event to participant agents), not a
230
+ // reason to poll.
238
231
  log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
239
232
  handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, log })
240
233
 
241
234
  return new Promise(() => {
242
235
  // Run until killed. Tidy up the socket on termination so a restarting
243
236
  // service doesn't leak a half-open connection.
244
- const bye = () => { clearInterval(sweep); stopTyping(); try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
237
+ const bye = () => { try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
245
238
  process.on('SIGTERM', bye)
246
239
  process.on('SIGINT', bye)
247
240
  })