@magpie-community/opencode-workbuddy-auth 0.1.45 → 0.1.47

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 +8 -5
  2. package/index.mjs +65 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -126,8 +126,8 @@ Each chat carries WorkBuddy's headers:
126
126
  - `Authorization`, `X-User-Id`, `X-Domain`, `X-Product: SaaS`, `X-IDE-Type: WorkBuddy`;
127
127
  - the desktop CLI's `User-Agent`, `WorkBuddy/<app> <appName>/<app> CLI/<cli>`:
128
128
  `WorkBuddy/5.9.3 WorkBuddy/5.9.3 CLI/2.165.0` on the China build and
129
- `WorkBuddy/5.8.4 workbuddy-ai/5.8.4 CLI/2.161.0` on WorkBuddy AI, each
130
- build's current app on 2026-10-28; `X-IDE-Version` is the app's version.
129
+ `WorkBuddy/5.9.3 workbuddy-ai/5.9.3 CLI/2.165.0` on WorkBuddy AI, each
130
+ build's current app on 2026-10-29; `X-IDE-Version` is the app's version.
131
131
  A User-Agent not starting `WorkBuddy/<version>` is answered error 10085;
132
132
  - the client's, which WorkBuddy's usage list shows as the client that asked
133
133
  (使用端): `X-Agent-Purpose: conversation`, `X-IDE-Name: WorkBuddy`,
@@ -137,14 +137,17 @@ Each chat carries WorkBuddy's headers:
137
137
  - the conversation's ids: `X-Conversation-ID`, `X-Conversation-Request-ID`,
138
138
  `X-Conversation-Message-ID`, `X-Request-ID`. WorkBuddy's backend counts a
139
139
  user send as one request by `X-Conversation-Request-ID`, so every step of
140
- one send — a tool result sent back, a retry, another account answering —
141
- carries the same one, and the user's next message carries another. The
140
+ one send — a tool result sent back, a retry — carries the same one, and
141
+ the user's next message carries another. Both ids are one account's own,
142
+ as WorkBuddy's client is signed in as one: a conversation sent as another
143
+ account (another of a group, or a sign-in saved since) carries that
144
+ account's ids, never the same ones under two `X-User-Id`s. The
142
145
  conversation's is the session magpie (or OpenCode) names the request by,
143
146
  else the chat's first user message; nothing is made up when neither names
144
147
  one, a made-up conversation of the request's own being what splits one
145
148
  conversation into many in the usage detail;
146
149
  - what the CLI adds to a model request alone: `X-Agent-Type: main` on both
147
- builds, `X-Agent-Runtime: local-agent` on the China build (new in 5.9),
150
+ builds, `X-Agent-Runtime: local-agent` (new in 5.9),
148
151
  `Accept: application/json`, and the `X-Stainless-*` headers of the OpenAI
149
152
  SDK it sends through (openai 6.25.0 on Electron 37's Node v22.21.1, a
150
153
  1200s timeout, no retries).
package/index.mjs CHANGED
@@ -92,11 +92,15 @@ const SITES = {
92
92
  authID: "workbuddy-desktop-ai",
93
93
  platform: "workbuddy-ai",
94
94
  ai: true,
95
- // WorkBuddy AI 5.8.4.41366630, its update feed's current build on
96
- // 2026-10-28 (see the China build's)
97
- app: "5.8.4",
98
- cli: "2.161.0",
95
+ // WorkBuddy AI 5.9.3.42398134, its update feed's current build on
96
+ // 2026-10-29 (see the China build's): the same CLI as the China
97
+ // build's 5.9.3, byte for byte but for product.json, and the same
98
+ // Electron (37.10.3); its desktop puts agentRuntime local-agent in the
99
+ // prompt meta as the China build's does
100
+ app: "5.9.3",
101
+ cli: "2.165.0",
99
102
  appName: "workbuddy-ai",
103
+ runtime: "local-agent",
100
104
  keys: "https://www.workbuddy.ai/profile/keys",
101
105
  models: AI_MODELS,
102
106
  // models whose images WorkBuddy AI drops though its config says they
@@ -442,7 +446,7 @@ async function enterpriseOf(site, a, who) {
442
446
  // itself, and the request that carried it. Its backend counts a user send as
443
447
  // one request by X-Conversation-Request-ID: a chat carrying a new one every
444
448
  // time is counted on its own, so one send that takes several steps — a tool
445
- // call answered and sent again, a retry, another account answering — shows up
449
+ // call answered and sent again, a retry — shows up
446
450
  // in the usage detail as many requests. The ids below are made from what
447
451
  // names the conversation and the turn, so every step of one send carries the
448
452
  // same turn id and the user's next message carries another, as WorkBuddy's
@@ -715,22 +719,64 @@ function sessionOf(headers) {
715
719
  // turn's (one user send, whatever it takes), and the conversation's. A header
716
720
  // already there — the caller's own, or another plugin's — is kept, and the
717
721
  // message and request ids are the same one, as WorkBuddy's client has them.
718
- function attend(headers, body, session) {
719
- attendChat(headers, chatOf(body), session)
722
+ //
723
+ // who is the account the chat goes as (scopeOf). The ids are that account's
724
+ // own: WorkBuddy's client runs as one account, so one conversation or turn
725
+ // never shows up under two. Without it, one magpie session spread over a
726
+ // group of accounts sent the same X-Conversation-ID and
727
+ // X-Conversation-Request-ID under every account's X-User-Id.
728
+ function attend(headers, body, session, who = "") {
729
+ return attendChat(headers, chatOf(body), session, who)
730
+ }
731
+
732
+ // attendChat is attend with the chat read already. It returns what reattend
733
+ // needs to set the ids again for another account once the chat itself is
734
+ // let go: the ids it set itself (not the caller's), and the digests of what
735
+ // names the turn and the conversation, which are never sent as they are.
736
+ function attendChat(headers, chat, session, who = "") {
737
+ const conv = conversationParts(chat, session)
738
+ const turn = turnParts(chat)
739
+ const ids = {
740
+ set: [],
741
+ // a turn is a conversation's own: two conversations whose user message
742
+ // reads the same are still two sends, not one
743
+ turn: turn.length ? idFor("turn", [...conv, "|", ...turn]) : "",
744
+ conv: conv.length ? idFor("conversation", conv) : "",
745
+ }
746
+ return setIds(headers, ids, who)
720
747
  }
721
748
 
722
- function attendChat(headers, chat, session) {
723
- const conv = conversationParts(chat, session)
749
+ // setIds sets the message's id, and the turn's and conversation's for the
750
+ // account who, where the caller set none.
751
+ function setIds(headers, ids, who) {
724
752
  const msg = hex()
725
753
  headers.set("X-Conversation-Message-ID", msg)
726
754
  headers.set("X-Request-ID", msg)
755
+ const set = []
756
+ const own = (kind, base) => (who ? idFor(kind, [`account:${who}`, "|", base]) : base)
727
757
  if (!headers.has("X-Conversation-Request-ID")) {
728
- const turn = turnParts(chat)
729
- // a turn is a conversation's own: two conversations whose user message
730
- // reads the same are still two sends, not one
731
- headers.set("X-Conversation-Request-ID", turn.length ? idFor("turn", [...conv, "|", ...turn]) : hex())
758
+ headers.set("X-Conversation-Request-ID", ids.turn ? own("turn", ids.turn) : hex())
759
+ set.push("X-Conversation-Request-ID")
760
+ }
761
+ if (ids.conv && !headers.has("X-Conversation-ID")) {
762
+ headers.set("X-Conversation-ID", own("conversation", ids.conv))
763
+ set.push("X-Conversation-ID")
732
764
  }
733
- if (conv.length && !headers.has("X-Conversation-ID")) headers.set("X-Conversation-ID", idFor("conversation", conv))
765
+ return { ...ids, set }
766
+ }
767
+
768
+ // scopeOf names the account a chat goes as for attend: its uid, its
769
+ // enterprise, and for a key account the key (only ever hashed).
770
+ function scopeOf(a) {
771
+ return `${a?.uid ?? ""}|${a?.enterpriseId ?? ""}|${a?.source === "key" ? a.key ?? "" : ""}`
772
+ }
773
+
774
+ // reattend sets a chat's ids again for the account it is now sent as, when
775
+ // that is another account than the one they were set for.
776
+ function reattend(headers, ids, was, now) {
777
+ if (was === now) return ids
778
+ for (const h of ids.set) headers.delete(h)
779
+ return setIds(headers, ids, now)
734
780
  }
735
781
 
736
782
  // FLAGGED are the words WorkBuddy refuses a chat for, "Illegal API
@@ -1328,7 +1374,7 @@ function fitted(body) {
1328
1374
 
1329
1375
  // gzipped is a chat's body compressed, as WorkBuddy's own client sends its
1330
1376
  // model requests: its CLI's GzipRequestProcessor (WorkBuddy 5.9.3, CLI
1331
- // 2.165.0; WorkBuddy AI 5.8.4 the same) gzips each POST's body and sets
1377
+ // 2.165.0, on both builds) gzips each POST's body and sets
1332
1378
  // Content-Encoding: gzip when the product's RequestBodyGzip feature is on,
1333
1379
  // which both builds' product.json turn on and /v3/config (merged over it)
1334
1380
  // doesn't turn off. Sent as plain JSON, a long agent conversation went up
@@ -2506,7 +2552,8 @@ function makePlugin(site) {
2506
2552
  const completion = !native && isCompletion(req ? req.url : input, chat)
2507
2553
  if (completion && chat && !namesEffort(chat) && withDefaultEffort(chat, await effortList(a))) changed = true
2508
2554
  chatHeaders(site, headers)
2509
- attendChat(headers, chat, session)
2555
+ let who = scopeOf(a)
2556
+ let ids = attendChat(headers, chat, session, who)
2510
2557
  const post = (b) => {
2511
2558
  // a chat goes gzipped, as WorkBuddy's client sends it
2512
2559
  // (gzipped); its size on the wire is what unanswered weighs
@@ -2553,6 +2600,7 @@ function makePlugin(site) {
2553
2600
  renewed = true
2554
2601
  a = r.a
2555
2602
  sign(site, a, headers)
2603
+ ids = reattend(headers, ids, who, (who = scopeOf(a)))
2556
2604
  const again = await post(b)
2557
2605
  if (again.status !== 401) return again
2558
2606
  return out("it refused the token renewed for it too", invalidToken(again), again)
@@ -2801,4 +2849,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
2801
2849
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
2802
2850
 
2803
2851
  // for tests
2804
- export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, 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, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
2852
+ export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
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",