@magpie-community/opencode-workbuddy-auth 0.1.27 → 0.1.29

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 (3) hide show
  1. package/README.md +12 -1
  2. package/index.mjs +85 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,7 +85,18 @@ default one, as the app does.
85
85
 
86
86
  WorkBuddy refuses some agents' identity lines as an "unapproved channel"
87
87
  (11128), so those lines are reworded before a chat goes out. Since 0.1.27
88
- (#279) that includes DeepSeek Harness's opening line.
88
+ (#279) that includes DeepSeek Harness's opening line. A refusal of words
89
+ not reworded yet is sent once more, since 0.1.29, with the system prompt
90
+ moved into the first user message (inside `<instructions>` tags) and
91
+ WorkBuddy's default system prompt in its place: WorkBuddy checks only the
92
+ system and assistant messages, and the line it matched is then in neither.
93
+ A system message later in the chat becomes a user message where it stands.
94
+ The plugin remembers each refused system prompt while it is loaded, so that
95
+ agent's next chats go moved at once. A refusal that stands after the move
96
+ (words in an assistant message) ends in a hint that names no one client
97
+ (since 0.1.28): WorkBuddy refused words in this client's prompt; use it from
98
+ Hermes, OpenCode or Pi, or add another provider to the group. Neither
99
+ refusal rests the account.
89
100
 
90
101
  The loader also translates OpenAI-compatible request spellings WorkBuddy
91
102
  doesn't take: `max_completion_tokens` becomes `max_tokens`, `developer`
package/index.mjs CHANGED
@@ -542,6 +542,61 @@ function withSystem(body) {
542
542
  }
543
543
  }
544
544
 
545
+ // WorkBuddy's channel check reads the system and assistant messages only:
546
+ // tried on 2026-10-21 with real accounts on both builds (glm-5.3,
547
+ // deepseek-v4.1-flash, gpt-5.5), each refused line was served in the user's
548
+ // message, a tool's result and a user message's parts, and refused again in
549
+ // the system or an assistant message. It matches a known line whole and
550
+ // exactly (one extra space lets it through), so FLAGGED only ever knows the
551
+ // lines met so far, and another agent's opening line is refused until one is
552
+ // added (Wing on Discord, 10-21, with an agent we couldn't name).
553
+ //
554
+ // systemAsUser is body with its system prompt read to the model as the
555
+ // first thing the user says, in INSTRUCTIONS' tags, and WorkBuddy's default
556
+ // system prompt first, as both builds want a system message there (11128
557
+ // "first message is not system prompt"). A system message later in the chat
558
+ // becomes the user's where it stands. null when there's nothing to move.
559
+ const DEFAULT_SYSTEM = "You are a helpful assistant."
560
+ const INSTRUCTIONS = ["<instructions>\n", "\n</instructions>"]
561
+
562
+ function textOf(content) {
563
+ if (typeof content === "string") return content
564
+ if (!Array.isArray(content)) return ""
565
+ return content.map((p) => (typeof p?.text === "string" ? p.text : "")).filter(Boolean).join("\n")
566
+ }
567
+
568
+ // systemKey is the text of a chat's system messages, which a chat refused
569
+ // once is known by: the same agent's next chat goes with systemAsUser at once
570
+ function systemKey(body) {
571
+ const msgs = chatOf(body)?.messages
572
+ if (!Array.isArray(msgs)) return ""
573
+ const key = msgs.filter((m) => m?.role === "system").map((m) => textOf(m.content)).join("\n")
574
+ return key.trim() === DEFAULT_SYSTEM ? "" : key
575
+ }
576
+
577
+ function systemAsUser(body) {
578
+ const b = chatOf(body)
579
+ const msgs = b?.messages
580
+ if (!Array.isArray(msgs)) return null
581
+ let i = 0
582
+ const lead = []
583
+ for (; i < msgs.length && msgs[i]?.role === "system"; i++) lead.push(textOf(msgs[i].content))
584
+ const said = lead.join("\n\n").trim()
585
+ const rest = msgs.slice(i).map((m) => (m?.role === "system" ? { ...m, role: "user" } : m))
586
+ const moved = said && said !== DEFAULT_SYSTEM
587
+ if (!moved && rest.every((m, k) => m === msgs[i + k])) return null
588
+ if (moved) {
589
+ const text = INSTRUCTIONS[0] + said + INSTRUCTIONS[1]
590
+ const u = rest.findIndex((m) => m?.role === "user")
591
+ if (u < 0) rest.unshift({ role: "user", content: text })
592
+ else if (typeof rest[u].content === "string") rest[u] = { ...rest[u], content: text + "\n\n" + rest[u].content }
593
+ else if (Array.isArray(rest[u].content)) rest[u] = { ...rest[u], content: [{ type: "text", text }, ...rest[u].content] }
594
+ else rest[u] = { ...rest[u], content: text }
595
+ }
596
+ b.messages = [{ role: "system", content: DEFAULT_SYSTEM }, ...rest]
597
+ return JSON.stringify(b)
598
+ }
599
+
545
600
  // normalizeToolChoice converts the object form emitted by OpenAI clients to
546
601
  // WorkBuddy's string form. It runs after fitted(), so a named choice follows a
547
602
  // tool name that was made safe for WorkBuddy.
@@ -1559,6 +1614,9 @@ function makePlugin(site) {
1559
1614
  // the models of this site that refused tool results then the user's
1560
1615
  // message, for this run: their next chats go with replyAfterTools at once
1561
1616
  const endsAfterTools = new Set()
1617
+ // the system prompts this run WorkBuddy refused as an unapproved channel:
1618
+ // their next chats go with systemAsUser at once, not refused first
1619
+ const refusedSystems = new Set()
1562
1620
  return async ({ client }) => ({
1563
1621
  config: async (config) => {
1564
1622
  config.provider ??= {}
@@ -1629,15 +1687,27 @@ function makePlugin(site) {
1629
1687
  // a model known to turn tool results then the user's message away
1630
1688
  // gets the empty reply between them up front (see endsAfterTools)
1631
1689
  const model = chatOf(body)?.model
1632
- const quiet = endsAfterTools.has(model) ? replyAfterTools(body) : null
1633
- let res = await send(quiet ?? body)
1634
- if (res.status === 400 && !quiet) {
1635
- const text = await res.text()
1636
- const again = AFTER_TOOLS_REFUSED.test(text) ? replyAfterTools(body) : null
1690
+ // a system prompt WorkBuddy refused before goes as the user's
1691
+ // words at once (see systemAsUser)
1692
+ const sysKey = systemKey(body)
1693
+ if (sysKey && refusedSystems.has(sysKey)) body = systemAsUser(body) ?? body
1694
+ const shaped = (b) => (endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
1695
+ let res = await send(shaped(body))
1696
+ if (res.status === 400) {
1697
+ let text = await res.text()
1698
+ const moved = REFUSED.test(text) && sysKey && !refusedSystems.has(sysKey) ? systemAsUser(body) : null
1699
+ if (moved) {
1700
+ refusedSystems.add(sysKey)
1701
+ if (refusedSystems.size > 256) refusedSystems.delete(refusedSystems.values().next().value)
1702
+ body = moved
1703
+ res = await send(shaped(body))
1704
+ if (res.status === 400) text = await res.text()
1705
+ }
1706
+ const again = res.status === 400 && !endsAfterTools.has(model) && AFTER_TOOLS_REFUSED.test(text) ? replyAfterTools(body) : null
1637
1707
  if (again) {
1638
1708
  endsAfterTools.add(model)
1639
1709
  res = await send(again)
1640
- } else {
1710
+ } else if (res.status === 400) {
1641
1711
  res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
1642
1712
  }
1643
1713
  }
@@ -1699,11 +1769,15 @@ function makePlugin(site) {
1699
1769
 
1700
1770
  // REFUSED_HINT is what the user can do about WorkBuddy's "Illegal API
1701
1771
  // invocation from an unapproved channel": both builds answer it to a chat
1702
- // holding words of Codex's or Claude Code's own (magpie #182), whatever the
1703
- // headers. FLAGGED rewrites the ones known; this is for one it doesn't know. It ends the error's message, as magpie's built-in
1704
- // put it (provider.WBRefusedHint), so magpie's routing page says it apart.
1772
+ // holding words they turn away, whatever the headers. Codex's and Claude
1773
+ // Code's were the first (magpie #182), DeepSeek Harness's and Claude
1774
+ // Desktop's came later (#279, #170), and WorkBuddy reads every message, so
1775
+ // it names no client. FLAGGED rewrites the words known; this is for ones it
1776
+ // doesn't know. It ends the error's message word for word as magpie's
1777
+ // Routing page has it (WB_REFUSED in its routing client.js), which says it
1778
+ // apart from WorkBuddy's own words.
1705
1779
  const REFUSED_HINT =
1706
- "WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
1780
+ "WorkBuddy refused words in this client's prompt (most often its system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
1707
1781
  const REFUSED = /unapproved channel|illegal api invocation/i
1708
1782
 
1709
1783
  // RESTRICTED_HINT is what the user can do about WorkBuddy's 403 code 11140
@@ -1781,4 +1855,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
1781
1855
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
1782
1856
 
1783
1857
  // for tests
1784
- export const _internal = { EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
1858
+ export const _internal = { systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",