oc-ghcp-headers 0.1.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.
Files changed (3) hide show
  1. package/README.md +83 -0
  2. package/index.ts +157 -0
  3. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # oc-ghcp-headers
2
+
3
+ This plugin controls the `x-initiator` header for GitHub Copilot requests in OpenCode.
4
+
5
+ ## Behavior
6
+
7
+ - First user message in a session: `x-initiator` is randomized between `user` and `agent`.
8
+ - Follow-up user messages: `x-initiator` defaults to `agent`, with optional chance to send `user`.
9
+
10
+ The plugin determines first vs follow-up by reading recent session messages and checking for prior assistant/tool activity.
11
+ It explicitly ignores the current turn's pre-created user message and assistant placeholder so first-turn detection stays accurate.
12
+
13
+ If session history cannot be loaded, it fails closed to `agent`.
14
+
15
+ ## Setup
16
+
17
+ Install from npm:
18
+
19
+ ```bash
20
+ bun add oc-ghcp-headers
21
+ ```
22
+
23
+ Then enable it in `~/.config/opencode/opencode.jsonc`:
24
+
25
+ ```json
26
+ {
27
+ "plugin": ["oc-ghcp-headers"]
28
+ }
29
+ ```
30
+
31
+ Restart OpenCode after updating config.
32
+
33
+ ## Configuration
34
+
35
+ Configure percentages in `opencode.json` / `opencode.jsonc` under `provider.github-copilot.options`:
36
+
37
+ ```json
38
+ {
39
+ "provider": {
40
+ "github-copilot": {
41
+ "options": {
42
+ "firstMessageAgentPercent": 0,
43
+ "followupMessageAgentPercent": 100
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ - `firstMessageAgentPercent` (default `0`)
51
+ - Percentage chance first message is sent as `agent`
52
+ - `0` => always `user`
53
+ - `10` => 10% `agent`, 90% `user`
54
+ - `100` => always `agent`
55
+ - `followupMessageAgentPercent` (default `100`)
56
+ - Percentage chance follow-up message is sent as `agent`
57
+ - `100` => always `agent`
58
+ - `90` => 90% `agent`, 10% `user`
59
+ - `0` => always `user`
60
+ - `DEBUG_ENABLED`
61
+ - Enable/disable plugin debug logging
62
+
63
+ ## Scope
64
+
65
+ - Applies only to models where `providerID` includes `github-copilot`.
66
+ - This plugin only sets request headers; auth/token handling remains managed by OpenCode.
67
+
68
+ ## Debug Log
69
+
70
+ - Log file: `/tmp/opencode-copilot-agent-header-debug.log`
71
+ - Watch live:
72
+
73
+ ```bash
74
+ tail -f /tmp/opencode-copilot-agent-header-debug.log
75
+ ```
76
+
77
+ ## Maintainer Docs
78
+
79
+ - Publishing and release process: `docs/release.md`
80
+
81
+ ## Credits
82
+
83
+ - Original custom plugin author: [@Tarquinen](https://github.com/Tarquinen/)
package/index.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+ import { appendFileSync } from "fs"
3
+
4
+ const DEBUG_ENABLED = true
5
+ const DEBUG_LOG = "/tmp/opencode-copilot-agent-header-debug.log"
6
+ const MESSAGE_LOOKBACK_LIMIT = 10
7
+ const DEFAULT_FIRST_MESSAGE_AGENT_PERCENT = 0
8
+ const DEFAULT_FOLLOWUP_MESSAGE_AGENT_PERCENT = 100
9
+
10
+ function formatError(error: unknown) {
11
+ if (error instanceof Error) return error.message
12
+ try {
13
+ return JSON.stringify(error)
14
+ } catch {
15
+ return String(error)
16
+ }
17
+ }
18
+
19
+ function log(message: string) {
20
+ if (!DEBUG_ENABLED) return
21
+ try {
22
+ const timestamp = new Date().toISOString()
23
+ appendFileSync(DEBUG_LOG, `${timestamp} ${message}\n`)
24
+ } catch {}
25
+ }
26
+
27
+ function getPercent(value: unknown, fallback: number) {
28
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback
29
+ if (value < 0) return 0
30
+ if (value > 100) return 100
31
+ return value
32
+ }
33
+
34
+ function sampleInitiator(agentPercent: number) {
35
+ const randomPercent = Math.random() * 100
36
+ return randomPercent < agentPercent ? "agent" : "user"
37
+ }
38
+
39
+ function hasAssistantOrToolMessages(messages: any[]) {
40
+ return messages.some((message) => {
41
+ const role = message?.info?.role
42
+ if (role === "assistant" || role === "tool") return true
43
+
44
+ if (Array.isArray(message?.parts)) {
45
+ return message.parts.some((part: any) => part?.type === "tool")
46
+ }
47
+
48
+ return false
49
+ })
50
+ }
51
+
52
+ function getPriorMessages(messages: any[], incoming: any) {
53
+ const currentMessageID = incoming?.message?.id
54
+ const currentCreated = incoming?.message?.time?.created
55
+
56
+ return messages.filter((message) => {
57
+ const info = message?.info
58
+ const messageID = info?.id
59
+
60
+ if (currentMessageID && messageID === currentMessageID) return false
61
+ if (currentMessageID && info?.role === "assistant" && info?.parentID === currentMessageID) return false
62
+
63
+ if (typeof currentCreated === "number") {
64
+ const created = info?.time?.created
65
+ if (typeof created === "number") {
66
+ return created < currentCreated
67
+ }
68
+ }
69
+
70
+ return true
71
+ })
72
+ }
73
+
74
+ async function loadSessionMessages(client: any, sessionID: string, directory: string | undefined) {
75
+ try {
76
+ const response = await client.session.messages({
77
+ path: { id: sessionID },
78
+ query: {
79
+ limit: MESSAGE_LOOKBACK_LIMIT,
80
+ ...(directory ? { directory } : {}),
81
+ },
82
+ throwOnError: true,
83
+ })
84
+
85
+ if (Array.isArray(response?.data)) {
86
+ return {
87
+ messages: response.data,
88
+ ok: true,
89
+ }
90
+ }
91
+
92
+ log("[HEADERS] session.messages returned no data")
93
+ } catch (error) {
94
+ const reason = formatError(error)
95
+ log(`[HEADERS] session.messages failed: ${reason}`)
96
+ }
97
+
98
+ return {
99
+ messages: [],
100
+ ok: false,
101
+ }
102
+ }
103
+
104
+ const CopilotForceAgentHeader: Plugin = async ({ client, directory }: any) => {
105
+ log("[INIT] Copilot Force Agent Header plugin loaded (chat.headers mode)")
106
+
107
+ return {
108
+ "chat.headers": async (incoming: any, output: any) => {
109
+ if (!incoming.model.providerID.includes("github-copilot")) return
110
+
111
+ const sessionID = incoming.sessionID ?? incoming.sessionId ?? incoming.session?.id
112
+
113
+ let isNonFirstMessage = false
114
+ let loadedHistory = false
115
+ const providerOptions = incoming?.provider?.options ?? {}
116
+ const firstMessageAgentPercent = getPercent(
117
+ providerOptions?.firstMessageAgentPercent,
118
+ DEFAULT_FIRST_MESSAGE_AGENT_PERCENT,
119
+ )
120
+ const followupMessageAgentPercent = getPercent(
121
+ providerOptions?.followupMessageAgentPercent,
122
+ DEFAULT_FOLLOWUP_MESSAGE_AGENT_PERCENT,
123
+ )
124
+
125
+ if (typeof sessionID === "string" && sessionID.length > 0) {
126
+ try {
127
+ const history = await loadSessionMessages(client, sessionID, directory)
128
+ const messages = history.messages
129
+ const priorMessages = getPriorMessages(messages, incoming)
130
+ loadedHistory = history.ok
131
+ isNonFirstMessage = hasAssistantOrToolMessages(priorMessages)
132
+ } catch (error) {
133
+ const reason = formatError(error)
134
+ log(`[HEADERS] Failed loading session messages: ${reason}`)
135
+ }
136
+ } else {
137
+ log("[HEADERS] Missing sessionID in incoming payload")
138
+ }
139
+
140
+ let initiator = "agent"
141
+
142
+ if (!loadedHistory) {
143
+ initiator = "agent"
144
+ } else if (isNonFirstMessage) {
145
+ initiator = sampleInitiator(followupMessageAgentPercent)
146
+ } else {
147
+ initiator = sampleInitiator(firstMessageAgentPercent)
148
+ }
149
+
150
+ output.headers ||= {}
151
+ output.headers["x-initiator"] = initiator
152
+ log(`[HEADERS] x-initiator=${initiator}`)
153
+ },
154
+ }
155
+ }
156
+
157
+ export default CopilotForceAgentHeader
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "oc-ghcp-headers",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that customizes GitHub Copilot x-initiator headers",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "README.md"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "type-check": "npx -p @typescript/native-preview tsgo --noEmit",
16
+ "prepublishOnly": "npm run type-check"
17
+ },
18
+ "keywords": [
19
+ "opencode",
20
+ "plugin",
21
+ "github-copilot",
22
+ "x-initiator"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@opencode-ai/plugin": "latest"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "^1.3.1",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }