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.
- package/README.md +83 -0
- package/index.ts +157 -0
- 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
|
+
}
|