openvisio-agent 0.6.0 → 0.6.2

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 +39 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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
@@ -40,7 +40,12 @@ const CHAT_CHARTER = [
40
40
  ].join('\n')
41
41
 
42
42
  const CYCLE = CHAT_CHARTER + '\n\nRun one OpenVisio autonomy cycle: call poll_inbox and handle mentions + follow-ups. Reply in 1-3 sentences, @mention people by their EXACT full name, at most one reply per channel. Then stop.'
43
- const CYCLE_FAST = CHAT_CHARTER + '\n\nNew chat activity. If a specific mention FOR YOU is given above, reply to it with mcp__openvisio-team__post_message (once). Then call poll_inbox and look at .followUps — but reply ONLY to the ones actually directed at YOU (a question to you, or a reply to your own message); SKIP thread chatter aimed at someone else or another agent. Ignore .tasks/.claimable. AT MOST ONE reply per channel; answer several nudges together in ONE post; never repeat a reply you already sent. If asked for work you have no tool for, say so plainly and offer to file a ticket. Never invent progress. Reply in 1-3 sentences, no summary. Then stop.'
43
+ const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
44
+ 'New chat activity. Do EXACTLY ONE of these:',
45
+ ' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with mcp__openvisio-team__post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — you already have the message; polling would make you re-answer it and double-post.',
46
+ ' • IF NO specific mention is given above: call poll_inbox and reply only to items truly directed at YOU (a question to you, or a reply to your own message) — SKIP chatter aimed at someone else / another agent, ignore .tasks/.claimable, at most one reply per channel.',
47
+ 'Post ONE message total for the thing you are answering — compose it fully, then send once. Never send a reply and then a "better" version; never repeat a reply you already sent. Be sure of your answer before sending. If asked for work you have no tool for, say so plainly and offer to file a ticket. 1-3 sentences, no summary. Then stop.',
48
+ ].join('\n')
44
49
 
45
50
  // ── CODE agents (--workdir given): full file + Bash + git/gh surface. ─────────
46
51
  // A stable "who you are / how you work" charter prepended to every code cycle.
@@ -75,10 +80,11 @@ const CODE_FULL = CODE_CHARTER + '\n\n' + [
75
80
  ].join('\n')
76
81
 
77
82
  const CODE_FAST = CODE_CHARTER + '\n\n' + [
78
- 'New chat activity. If a specific mention FOR YOU is given above, reply to it FIRST with mcp__openvisio-team__post_message (once) — before any Bash.',
79
- 'Then poll_inbox and handle .followUps ONLY when the reply is directed at YOU (asks you something, or responds to your own message) SKIP thread chatter aimed at someone else / another agent. At most one reply per channel, and never repeat a reply you already sent.',
80
- 'If a message asks YOU for real CODE work, do the WHOLE job now branch (checkout -B agent/<slug>), edit, run tests, commit, push your branch, open a PR (gh pr create), then reply with the PR link and @mention the requester by exact full name. Never push to main, never --force, never merge.',
81
- 'Do NOT promise and stopfinish and report in THIS cycle. Reply 1-3 sentences, no summary. Then stop.',
83
+ 'New chat activity. Do EXACTLY ONE of these:',
84
+ ' IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with mcp__openvisio-team__post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle polling would re-surface the same message and make you double-post.',
85
+ ' IF NO specific mention is given above: call poll_inbox and reply only to items directed at YOU (asks you something, or responds to your own message) SKIP chatter aimed at someone else / another agent; at most one reply per channel.',
86
+ 'If the message asks YOU for real CODE work, do the WHOLE job now branch (checkout -B agent/<slug>), edit, run tests, commit, push your branch, open a PR (gh pr create), then reply once with the PR link and @mention the requester by exact full name. Never push to main, never --force, never merge.',
87
+ 'Post ONE message for the thing you are answering — send it once; never a reply then a "better" version; never repeat a reply you already sent; be sure before you send. Do NOT promise and stop — finish and report in THIS cycle. 1-3 sentences, no summary. Then stop.',
82
88
  ].join('\n')
83
89
 
84
90
  // ── Workspace-ethics cycles (both chat-only + code agents) ───────────────────
@@ -344,21 +350,24 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, mcpConfig, wor
344
350
  if (x === 'opus' || x === 'sonnet' || x === 'haiku') return x
345
351
  return /^claude-[a-z0-9.\-\[\]]+$/i.test(x) ? x : null
346
352
  }
353
+ // Optional target tier: "chat"/"lite" → chat cycles only, "code"/"full" → code
354
+ // cycles only, absent → both.
355
+ const parseTarget = (t) => t && /^(chat|lite)$/i.test(t) ? 'chat' : t && /^(code|full)$/i.test(t) ? 'code' : 'both'
347
356
  const parseModelCmd = (text) => {
348
- // Slash form is unambiguous: "/model" (report) or "/model <name>".
349
- const slash = /(?:^|\s)\/model\b(?:\s+(\S+))?/i.exec(text)
350
- if (slash) { const a = slash[1]; if (!a) return { report: true }; const n = normalizeModel(a); return n ? { set: n } : { invalid: a } }
357
+ // Slash form is unambiguous: "/model" (report), "/model <name>", "/model chat <name>".
358
+ const slash = /(?:^|\s)\/model\b(?:\s+(chat|lite|code|full))?(?:\s+(\S+))?/i.exec(text)
359
+ if (slash) { const name = slash[2]; if (!name) return { report: true }; const n = normalizeModel(name); return n ? { set: n, target: parseTarget(slash[1]) } : { invalid: name } }
351
360
  // Natural language: only when a VALID model name is explicitly given after the
352
361
  // word "model" — so "use the model file in the repo" does NOT switch anything.
353
- const nl = /\b(?:set|switch|change|use)\s+(?:the\s+|your\s+)?model\s+(?:to\s+)?(\S+)/i.exec(text)
354
- if (nl) { const n = normalizeModel(nl[1]); if (n) return { set: n } }
362
+ const nl = /\b(?:set|switch|change|use)\s+(?:the\s+|your\s+)?(chat|code)?\s*model\s+(?:to\s+)?(\S+)/i.exec(text)
363
+ if (nl) { const n = normalizeModel(nl[2]); if (n) return { set: n, target: parseTarget(nl[1]) } }
355
364
  if (/\b(?:which|what)\s+model\s+(?:are\s+you|do\s+you|you\b)/i.test(text)) return { report: true }
356
365
  return null
357
366
  }
358
- // Persist a model change so a restart / the background service keeps it.
359
- const persistModel = (mdl) => {
367
+ // Persist the current model tiers so a restart / the background service keeps them.
368
+ const persistModel = () => {
360
369
  if (!slug) return
361
- try { writeJson(configPath(slug), { ...(readConfig(slug) || {}), model: mdl, chatModel: mdl }, true) } catch { /* best-effort */ }
370
+ try { writeJson(configPath(slug), { ...(readConfig(slug) || {}), model: codeModel, chatModel: liteModel !== codeModel ? liteModel : '' }, true) } catch { /* best-effort */ }
362
371
  }
363
372
 
364
373
  function onEvent(k, d) {
@@ -393,32 +402,36 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, mcpConfig, wor
393
402
  const parent = msg.parent_id != null ? msg.parent_id : (msg.parentId != null ? msg.parentId : null)
394
403
  const threadRoot = parent != null ? parent : mid
395
404
  // De-dupe: the same mention re-delivered (reconnect replay / dup fan-out) must
396
- // NOT trigger a second reply to the same message.
397
- if (mid != null) {
398
- if (seenMentions.has(mid)) { log('agent:mention (dup ' + mid + ') skipped'); return }
399
- seenMentions.add(mid); if (seenMentions.size > 500) seenMentions.clear()
400
- }
405
+ // NOT trigger a second reply. Key by message id, or a channel+text signature
406
+ // when the payload carries no id.
407
+ const dedupeKey = mid != null ? 'id:' + mid : 'sig:' + (cid != null ? cid : '?') + '|' + text.slice(0, 100)
408
+ if (seenMentions.has(dedupeKey)) { log('agent:mention (dup) — skipped'); return }
409
+ seenMentions.add(dedupeKey); if (seenMentions.size > 500) seenMentions.clear()
401
410
  // Under-the-hood model control from chat (view / switch the model the agent runs).
402
411
  const mcmd = cid != null ? parseModelCmd(text) : null
403
412
  if (mcmd) {
404
413
  const thread = threadRoot != null ? `, parent_id ${threadRoot}` : ''
405
414
  if (mcmd.report) {
406
- log('model query → ' + codeModel + (liteModel !== codeModel ? ' / chat ' + liteModel : ''))
407
- void drain('fast', `An engineer asked which model you're running. Reply once in channel ${cid}${thread} (with your agent creds): "I'm currently running on ${codeModel}${liteModel !== codeModel ? ` (chat replies on ${liteModel})` : ''}." One line. Then stop.`)
415
+ log('model query → code ' + codeModel + ' / chat ' + liteModel)
416
+ void drain('fast', `An engineer asked which model you're running. Reply once in channel ${cid}${thread} (with your agent creds): "I'm on ${codeModel} for code work${liteModel !== codeModel ? ` and ${liteModel} for chat replies` : ' (and chat)'}." One line. Then stop.`)
408
417
  } else if (mcmd.invalid) {
409
418
  void drain('fast', `An engineer tried to switch your model to "${mcmd.invalid}", which isn't one you recognize. Reply once in channel ${cid}${thread} (with your agent creds): say you support "opus", "sonnet", "haiku", or a full "claude-…" id, and ask which they meant. One line. Then stop.`)
410
419
  } else {
411
- const prev = codeModel
412
- codeModel = mcmd.set; liteModel = mcmd.set
413
- persistModel(mcmd.set)
414
- log('model switched ' + prev + '' + codeModel + (who ? ' (by ' + who + ')' : ''))
415
- void drain('fast', `An engineer switched your underlying model to "${codeModel}" it is now active for your next actions. Post ONE short confirmation in channel ${cid}${thread} (with your agent creds): e.g. "Switched to ${codeModel} — I'll run on it from here." Then stop.`)
420
+ const tgt = mcmd.target // 'chat' | 'code' | 'both'
421
+ const prev = `code ${codeModel}/chat ${liteModel}`
422
+ if (tgt === 'chat') liteModel = mcmd.set
423
+ else if (tgt === 'code') codeModel = mcmd.set
424
+ else { codeModel = mcmd.set; liteModel = mcmd.set }
425
+ persistModel()
426
+ const label = tgt === 'chat' ? 'chat model' : tgt === 'code' ? 'code model' : 'model'
427
+ log('model switched (' + tgt + ') ' + prev + ' → code ' + codeModel + '/chat ' + liteModel + (who ? ' (by ' + who + ')' : ''))
428
+ void drain('fast', `An engineer switched your ${label} to "${mcmd.set}" — active for your next ${tgt === 'chat' ? 'chat replies' : tgt === 'code' ? 'code cycles' : 'actions'}. Post ONE short confirmation in channel ${cid}${thread} (with your agent creds): e.g. "Switched my ${label} to ${mcmd.set} — I'll use it from here." Then stop.`)
416
429
  }
417
430
  return
418
431
  }
419
432
  log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
420
433
  const ctx = cid != null
421
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU reply once 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. FIRST read the recent messages in this thread: if you already answered this, or it turns out another agent was the one addressed, do NOT post. Be sure of your answer before sending — do not reply then correct yourself.${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.`
434
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with mcp__openvisio-team__post_message — 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. Compose the whole answer, then post it ONCE — do NOT post a first reply and then a revised/"better" one. FIRST read the recent messages in this thread: if you already answered this, or another agent was the one addressed, do NOT post at all. Be sure of your answer before sending.${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, and after your single reply, STOP.`
422
435
  : undefined
423
436
  void drain('fast', ctx)
424
437
  } else if (k === 'error') {