@wenrwa/marketplace-cli 0.7.8 → 0.8.1
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/dist/__tests__/bridge-activity-tail.test.js +16 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
- package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-engine.test.js +112 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
- package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
- package/dist/__tests__/bridge-crew-translator.test.js +103 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
- package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +179 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
- package/dist/__tests__/bridge-server.test.js +32 -0
- package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
- package/dist/__tests__/opencode-config-parity.test.js +85 -0
- package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
- package/dist/__tests__/opencode-plugin.test.js +197 -0
- package/dist/__tests__/opencode-title.test.d.ts +1 -0
- package/dist/__tests__/opencode-title.test.js +99 -0
- package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
- package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
- package/dist/bridge/activityTail.d.ts +7 -0
- package/dist/bridge/activityTail.js +1 -0
- package/dist/bridge/agentLaunch.d.ts +28 -0
- package/dist/bridge/agentLaunch.js +49 -0
- package/dist/bridge/attachmentWriter.d.ts +16 -0
- package/dist/bridge/attachmentWriter.js +24 -1
- package/dist/bridge/bundleInstaller.d.ts +3 -0
- package/dist/bridge/bundleInstaller.js +22 -8
- package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
- package/dist/bridge/claudeTranscriptReader.js +12 -2
- package/dist/bridge/contextFile.d.ts +20 -0
- package/dist/bridge/contextFile.js +30 -0
- package/dist/bridge/contextWriter.d.ts +10 -1
- package/dist/bridge/contextWriter.js +29 -9
- package/dist/bridge/crewTranslator.d.ts +41 -0
- package/dist/bridge/crewTranslator.js +129 -0
- package/dist/bridge/launchSupervisor.d.ts +4 -0
- package/dist/bridge/opencodeConfig.d.ts +41 -0
- package/dist/bridge/opencodeConfig.js +107 -0
- package/dist/bridge/opencodePlugin.d.ts +13 -0
- package/dist/bridge/opencodePlugin.js +54 -0
- package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
- package/dist/bridge/opencodeTranscriptReader.js +178 -0
- package/dist/bridge/relayClient.d.ts +27 -1
- package/dist/bridge/relayClient.js +24 -7
- package/dist/bridge/screenshotAttach.d.ts +2 -0
- package/dist/bridge/screenshotAttach.js +3 -1
- package/dist/bridge/server.d.ts +6 -0
- package/dist/bridge/server.js +9 -1
- package/dist/bridge/sessionManager.d.ts +5 -0
- package/dist/bridge/sessionManager.js +21 -1
- package/dist/commands/bridge.js +36 -9
- package/hooks/opencode-plugin.mjs +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// HiveOp OpenCode activity plugin (oc-06). Self-contained ESM: no imports beyond node:*.
|
|
2
|
+
// Writes the SAME JSONL lines as the Claude Code hooks (hooks/post-tool-use.js, stop.js)
|
|
3
|
+
// to $HIVEOP_ACTIVITY_FILE, which the bridge already tails. Copies of this file live in
|
|
4
|
+
// infra/terminal-image/opencode/ (hosted) and cli/hooks/ (local); a test keeps them identical.
|
|
5
|
+
//
|
|
6
|
+
// Event -> line:
|
|
7
|
+
// tool.execute.before / .after -> kind 'tool' (working, thinking "<Verb> <file>")
|
|
8
|
+
// session.idle, session.status idle -> kind 'stop' (idle, thinking '')
|
|
9
|
+
// permission.asked, question.asked -> kind 'needs_you' (thinking 'Waiting for you')
|
|
10
|
+
// permission.replied -> kind 'tool', tool '' (working, thinking 'Working')
|
|
11
|
+
// session.updated (primary session, real title) -> OSC 0 pane title (oc-17), written to /dev/tty
|
|
12
|
+
// because OpenCode's own title ("OpenCode" / "OC | <title>") is switched off by the launch
|
|
13
|
+
// (OPENCODE_DISABLE_TERMINAL_TITLE=1): the app's auto-title ingestion shows the title verbatim.
|
|
14
|
+
// No-op without HIVEOP_ACTIVITY_FILE and WENRWA_SESSION_ID; nothing ever throws into OpenCode.
|
|
15
|
+
// ONLY plugin functions may be exported: OpenCode loads every export as a plugin, and a helper
|
|
16
|
+
// export breaks every prompt (Plugin.trigger reads hooks off its return value).
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
// OpenCode tool ids -> the Claude Code names the bridge/touch tracker already understand.
|
|
21
|
+
const TOOL_NAME = {
|
|
22
|
+
read: 'Read', edit: 'Edit', write: 'Write', patch: 'MultiEdit', multiedit: 'MultiEdit', bash: 'Bash',
|
|
23
|
+
}
|
|
24
|
+
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Update', 'Create', 'MultiEdit', 'NotebookEdit'])
|
|
25
|
+
const VERB = {
|
|
26
|
+
Read: 'Reading', Edit: 'Editing', Write: 'Writing', Update: 'Updating',
|
|
27
|
+
Create: 'Creating', MultiEdit: 'Editing', NotebookEdit: 'Editing', Bash: 'Running',
|
|
28
|
+
}
|
|
29
|
+
const MAX_BASH_PATHS = 5
|
|
30
|
+
|
|
31
|
+
// Same algorithm as hooks/post-tool-use.js extractBashPaths (kept in step by a parity test).
|
|
32
|
+
function extractBashPaths(command, pathExists, cwd) {
|
|
33
|
+
if (!command || typeof command !== 'string') return []
|
|
34
|
+
const tokens = command.match(/"[^"]*"|'[^']*'|[^\s;|&()<>]+/g) || []
|
|
35
|
+
const root = (cwd || '').replace(/\/+$/, '')
|
|
36
|
+
const out = []
|
|
37
|
+
const seen = new Set()
|
|
38
|
+
const bare = (s) => s.replace(/^['"]|['"]$/g, '').replace(/[),;]+$/, '')
|
|
39
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
40
|
+
const prev = bare(tokens[i - 1])
|
|
41
|
+
if (prev === 'cd' || prev === 'pushd') continue
|
|
42
|
+
const t = bare(tokens[i])
|
|
43
|
+
if (!t || t.startsWith('-')) continue
|
|
44
|
+
const norm = t.replace(/\/+$/, '')
|
|
45
|
+
if (!norm || seen.has(norm)) continue
|
|
46
|
+
if (norm === '.' || (root && norm === root)) continue
|
|
47
|
+
let ok = false
|
|
48
|
+
try { ok = !!pathExists(t) } catch { ok = false }
|
|
49
|
+
if (!ok) continue
|
|
50
|
+
seen.add(norm)
|
|
51
|
+
out.push(t)
|
|
52
|
+
if (out.length >= MAX_BASH_PATHS) break
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exists = (p) => { try { return fs.existsSync(p) } catch { return false } }
|
|
58
|
+
|
|
59
|
+
/** Build the 'tool' record fields for an OpenCode tool call. */
|
|
60
|
+
function toolRecord(toolId, args) {
|
|
61
|
+
const tool = typeof toolId === 'string' ? (TOOL_NAME[toolId.toLowerCase()] || toolId) : ''
|
|
62
|
+
const a = args && typeof args === 'object' ? args : {}
|
|
63
|
+
const paths = []
|
|
64
|
+
for (const k of ['filePath', 'file_path', 'notebook_path']) if (typeof a[k] === 'string') paths.push(a[k])
|
|
65
|
+
const filePaths = FILE_TOOLS.has(tool)
|
|
66
|
+
? paths.filter((p, i) => p && paths.indexOf(p) === i)
|
|
67
|
+
: tool === 'Bash' ? extractBashPaths(a.command, exists, process.cwd()) : []
|
|
68
|
+
const base = filePaths.length ? String(filePaths[0]).split('/').pop() : ''
|
|
69
|
+
return { kind: 'tool', tool, filePaths, thinking: (VERB[tool] || 'Working') + (base ? ' ' + base : '') }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function append(extra, env) {
|
|
73
|
+
try {
|
|
74
|
+
const file = env.HIVEOP_ACTIVITY_FILE
|
|
75
|
+
const sessionId = env.WENRWA_SESSION_ID || ''
|
|
76
|
+
if (!file || !sessionId) return
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
78
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId, workspaceId: env.WENRWA_WORKSPACE_ID || '', ...extra }) + '\n')
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// OpenCode's placeholder titles until the title generator runs ("New session - <iso>").
|
|
83
|
+
const DEFAULT_TITLE = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T/
|
|
84
|
+
const MAX_TITLE = 80
|
|
85
|
+
|
|
86
|
+
/** oc-17: the OSC 0 sequence for a session.updated event's info, or '' (sub-agent, placeholder, empty). */
|
|
87
|
+
function titleSequence(info) {
|
|
88
|
+
if (!info || info.parentID || typeof info.title !== 'string') return ''
|
|
89
|
+
const t = info.title.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
90
|
+
if (!t || DEFAULT_TITLE.test(t)) return ''
|
|
91
|
+
return '\x1b]0;' + (t.length > MAX_TITLE ? t.slice(0, MAX_TITLE - 1) + '\u2026' : t) + '\x07'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let lastTitle = ''
|
|
95
|
+
function setPaneTitle(info, ttyPath = '/dev/tty') {
|
|
96
|
+
try {
|
|
97
|
+
const seq = titleSequence(info)
|
|
98
|
+
if (!seq || seq === lastTitle) return
|
|
99
|
+
const fd = fs.openSync(ttyPath, 'a')
|
|
100
|
+
try { fs.writeSync(fd, seq) } finally { fs.closeSync(fd) }
|
|
101
|
+
lastTitle = seq
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let reportedToGateway = ''
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* oc-05 (hosted): POST the primary session's id to $HIVEOP_GATEWAY_URL/hiveop/session, authenticated
|
|
109
|
+
* by the per-agent gateway token (it already identifies the agent). Best-effort: one retry, never
|
|
110
|
+
* throws, no-op without gateway env. Last one wins server-side, so a fresh session after a
|
|
111
|
+
* "Session not found" fallback replaces the dead id.
|
|
112
|
+
*/
|
|
113
|
+
async function reportSessionToGateway(id, env = process.env) {
|
|
114
|
+
try {
|
|
115
|
+
const base = String(env.HIVEOP_GATEWAY_URL || '').replace(/\/+$/, '')
|
|
116
|
+
const token = env.HIVEOP_GATEWAY_TOKEN || ''
|
|
117
|
+
if (!id || !base || !token || id === reportedToGateway) return
|
|
118
|
+
reportedToGateway = id // claimed up front so a burst of events posts once; released on failure
|
|
119
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${base}/hiveop/session`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
124
|
+
body: JSON.stringify({ sessionId: id }),
|
|
125
|
+
signal: AbortSignal.timeout(5000),
|
|
126
|
+
})
|
|
127
|
+
if (res.ok) return
|
|
128
|
+
if (res.status < 500) break
|
|
129
|
+
} catch {}
|
|
130
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
131
|
+
}
|
|
132
|
+
if (reportedToGateway === id) reportedToGateway = ''
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* oc-05 hook point: capture the generated id of an agent's PRIMARY session so the next launch
|
|
138
|
+
* can `opencode -s <id>`. Sub-agent sessions (info.parentID) never overwrite it.
|
|
139
|
+
* Local lane: first id is written to a file under HIVEOP_OPENCODE_SESSIONS_DIR (keyed by agent).
|
|
140
|
+
* Hosted lane: the id is POSTed to the gateway (see reportSessionToGateway).
|
|
141
|
+
*/
|
|
142
|
+
function captureSessionId(event, env = process.env) {
|
|
143
|
+
try {
|
|
144
|
+
const p = (event && event.properties) || {}
|
|
145
|
+
if (!event || event.type !== 'session.created' || (p.info && p.info.parentID)) return
|
|
146
|
+
const id = p.sessionID || (p.info && p.info.id) || ''
|
|
147
|
+
void reportSessionToGateway(id, env)
|
|
148
|
+
const agentId = env.WENRWA_AGENT_ID || ''
|
|
149
|
+
if (!agentId) return
|
|
150
|
+
const dir = env.HIVEOP_OPENCODE_SESSIONS_DIR || '/data/.hiveop/opencode-sessions'
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
152
|
+
const f = path.join(dir, agentId)
|
|
153
|
+
if (!fs.existsSync(f)) fs.writeFileSync(f, id)
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const HiveOp = async () => {
|
|
158
|
+
const env = process.env
|
|
159
|
+
let lastKind = ''
|
|
160
|
+
const emit = (extra) => {
|
|
161
|
+
// session.status idle and session.idle both fire per turn end: write one stop line.
|
|
162
|
+
if (extra.kind === 'stop' && lastKind === 'stop') return
|
|
163
|
+
lastKind = extra.kind
|
|
164
|
+
append({ ...extra, ...(extra.kind === 'stop' ? { thinking: '' } : {}) }, env)
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
event: async ({ event }) => {
|
|
168
|
+
try {
|
|
169
|
+
if (!event) return
|
|
170
|
+
const p = event.properties || {}
|
|
171
|
+
if (event.type === 'session.created') captureSessionId(event, env)
|
|
172
|
+
else if (event.type === 'session.updated') setPaneTitle(p.info, env.HIVEOP_TITLE_TTY || '/dev/tty')
|
|
173
|
+
else if (event.type === 'session.idle') emit({ kind: 'stop' })
|
|
174
|
+
else if (event.type === 'session.status' && p.status && p.status.type === 'idle') emit({ kind: 'stop' })
|
|
175
|
+
else if (event.type === 'permission.asked' || event.type === 'question.asked') emit({ kind: 'needs_you', thinking: 'Waiting for you' })
|
|
176
|
+
else if (event.type === 'permission.replied') emit({ kind: 'tool', tool: '', filePaths: [], thinking: 'Working' })
|
|
177
|
+
} catch {}
|
|
178
|
+
},
|
|
179
|
+
'tool.execute.before': async (input, output) => {
|
|
180
|
+
try { emit(toolRecord(input && input.tool, (output && output.args) || (input && input.args))) } catch {}
|
|
181
|
+
},
|
|
182
|
+
'tool.execute.after': async (input, output) => {
|
|
183
|
+
try { emit(toolRecord(input && input.tool, (input && input.args) || (output && output.args))) } catch {}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|