pantheon-opencode 1.2.0 → 1.2.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.
Files changed (43) hide show
  1. package/README.md +101 -3
  2. package/bin/pantheon-init.mjs +79 -0
  3. package/commands/pantheon-vision.md +67 -0
  4. package/opencode.json +13 -9
  5. package/package.json +12 -5
  6. package/scripts/_pantheon_paths.py +0 -2
  7. package/scripts/doctor.mjs +4 -2
  8. package/scripts/install/health-check.mjs +101 -1
  9. package/scripts/install/model-picker.mjs +118 -0
  10. package/scripts/install/opencode.mjs +102 -19
  11. package/scripts/install/venv.mjs +25 -2
  12. package/scripts/install-mcp.mjs +98 -13
  13. package/scripts/mcp_persistence_server.py +1 -1
  14. package/scripts/secret-scan.mjs +88 -0
  15. package/scripts/uninstall.mjs +3 -3
  16. package/scripts/validate-routing.mjs +15 -0
  17. package/src/agents/hermes.md +2 -2
  18. package/src/agents/mnemosyne.md +1 -1
  19. package/src/agents/zeus.md +1 -1
  20. package/src/mcp/_pantheon_paths.py +0 -2
  21. package/src/mcp/install-mcp.mjs +6 -913
  22. package/src/mcp/mcp_persistence_server.py +2 -1
  23. package/src/mcp/pantheon_vision_server.py +495 -0
  24. package/src/mcp/requirements-vision.txt +3 -0
  25. package/src/pantheon/auto-wake.ts +91 -0
  26. package/src/pantheon/background-job-board.ts +568 -0
  27. package/src/pantheon/file-persistence.ts +95 -0
  28. package/src/pantheon/presets.d.mts +108 -0
  29. package/src/pantheon/presets.mjs +554 -0
  30. package/src/pantheon/vision.ts +1437 -0
  31. package/src/plugin.ts +69 -25
  32. package/src/plugins/tui/package.json +3 -3
  33. package/src/plugins/tui/src/index.tsx +866 -121
  34. package/src/routing.yml +148 -0
  35. package/src/skills/context-compression/SKILL.md +1 -1
  36. package/src/skills/loop-engineering/SKILL.md +1 -1
  37. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  38. package/scripts/scrub_secrets.py +0 -281
  39. package/src/plugins/tui/dist/tui.d.ts +0 -8
  40. package/src/plugins/tui/dist/tui.d.ts.map +0 -1
  41. package/src/plugins/tui/dist/tui.js +0 -609
  42. package/src/plugins/tui/dist/tui.js.map +0 -1
  43. package/src/plugins/tui/dist/tui.tsx +0 -444
@@ -1,444 +0,0 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: necessary for TuiPluginApi dynamic state access
2
- // biome-ignore-all lint/a11y/noStaticElementInteractions: TUI elements, not DOM
3
- /** @jsxImportSource @opentui/solid */
4
-
5
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from '@opencode-ai/plugin/tui'
6
- import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
7
-
8
- /* ─── Constants ─────────────────────────────────────────── */
9
-
10
- const BAR_WIDTH = 20
11
-
12
- const COMMANDS = [
13
- { name: '/pantheon', desc: 'Council synthesis' },
14
- { name: '/pantheon-status', desc: 'System status' },
15
- { name: '/pantheon-audit', desc: 'Full audit' },
16
- { name: '/pantheon-bg', desc: 'List background tasks' },
17
- { name: '/pantheon-cancel', desc: 'Cancel task' },
18
- { name: '/pantheon-deepwork', desc: 'Deep work mode' },
19
- { name: '/pantheon-focus', desc: 'Focus on scope' },
20
- { name: '/pantheon-remember', desc: 'Memory store/recall' },
21
- { name: '/pantheon-search', desc: 'Memory search' },
22
- { name: '/pantheon-consolidate', desc: 'Merge memories' },
23
- { name: '/pantheon-forget', desc: 'Compress memories' },
24
- ] as const
25
-
26
- const AGENTS = [
27
- { name: 'zeus', tier: 'default' as const, role: 'Orchestrator' },
28
- { name: 'athena', tier: 'premium' as const, role: 'Strategic planner' },
29
- { name: 'apollo', tier: 'fast' as const, role: 'Codebase discovery' },
30
- { name: 'hermes', tier: 'default' as const, role: 'Backend' },
31
- { name: 'aphrodite', tier: 'default' as const, role: 'Frontend' },
32
- { name: 'demeter', tier: 'default' as const, role: 'Database' },
33
- { name: 'themis', tier: 'premium' as const, role: 'Quality & security' },
34
- { name: 'prometheus', tier: 'default' as const, role: 'Infrastructure' },
35
- { name: 'hephaestus', tier: 'default' as const, role: 'AI pipelines' },
36
- { name: 'nyx', tier: 'fast' as const, role: 'Observability' },
37
- { name: 'gaia', tier: 'fast' as const, role: 'Remote sensing' },
38
- { name: 'iris', tier: 'fast' as const, role: 'GitHub operations' },
39
- { name: 'mnemosyne', tier: 'fast' as const, role: 'Memory bank' },
40
- { name: 'talos', tier: 'fast' as const, role: 'Hotfixes' },
41
- ] as const
42
-
43
- /* ─── Helpers ───────────────────────────────────────────── */
44
-
45
- function safeNum(v: unknown): number {
46
- return typeof v === 'number' && Number.isFinite(v) ? v : 0
47
- }
48
-
49
- function fmtInt(n: number): string {
50
- return Intl.NumberFormat('en-US').format(Math.max(0, Math.round(n)))
51
- }
52
-
53
- function fmtCost(v: number): string {
54
- if (v <= 0) return ''
55
- if (v < 0.01) return '<$0.01'
56
- return `$${v.toFixed(2)}`
57
- }
58
-
59
- function msgTokens(msg: any): number {
60
- const t = msg?.tokens ?? msg?.info?.tokens ?? {}
61
- return safeNum(t.input) + safeNum(t.output) + safeNum(t.reasoning)
62
- + safeNum(t?.cache?.read) + safeNum(t?.cache?.write)
63
- }
64
-
65
- function msgCost(src: any): number {
66
- for (const c of [src?.cost, src?.info?.cost, src?.usage?.cost]) {
67
- if (typeof c === 'number' && c > 0) return c
68
- }
69
- return 0
70
- }
71
-
72
- function buildBar(pct: number): string {
73
- const clamped = Math.max(0, Math.min(100, pct))
74
- const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((clamped / 100) * BAR_WIDTH)))
75
- return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
76
- }
77
-
78
- /* ─── Version Detection ────────────────────────────────────
79
- * 100% dinâmico — zero fallback hardcoded.
80
- * Se falhar, retorna null e a View omite a versão.
81
- *
82
- * Try order:
83
- * 1. api.client.file.read package.json
84
- * 2. git describe --tags
85
- * 3. opencode --version
86
- * 4. null */
87
-
88
- async function detectVersion(api: TuiPluginApi): Promise<string | null> {
89
- // Try 1: package.json do projeto (via worktree path)
90
- try {
91
- const wt = ((api.state as any).path?.worktree ?? '') as string
92
- const fp = wt ? `${wt}/package.json` : 'package.json'
93
- const result = await api.client.file.read({ query: { path: fp } })
94
- const match = String(result?.content ?? '').match(/"version":\s*"([^"]+)"/)
95
- if (match?.[1]) return match[1]
96
- } catch { /* fall through */ }
97
-
98
- // Try 2: git describe --tags
99
- try {
100
- const proc = (api as any).client?.process
101
- if (typeof proc?.exec === 'function') {
102
- const r = await proc.exec({ command: 'git', args: ['describe', '--tags', '--always'], timeoutMs: 3000 })
103
- const stdout = (r.stdout ?? r.output ?? '') as string
104
- const tag = stdout.trim().replace(/^v/, '').replace(/-\d+-g[0-9a-f]+$/, '')
105
- if (tag && tag !== '5.0.0') return tag
106
- }
107
- } catch { /* fall through */ }
108
-
109
- // Try 3: opencode --version
110
- try {
111
- const proc = (api as any).client?.process
112
- if (typeof proc?.exec === 'function') {
113
- const r = await proc.exec({ command: 'opencode', args: ['--version'], timeoutMs: 3000 })
114
- const stdout = (r.stdout ?? r.output ?? '') as string
115
- const ver = stdout.trim().replace(/^v/, '')
116
- if (ver) return ver
117
- }
118
- } catch { /* fall through */ }
119
-
120
- return null // sem fallback — versão não aparece
121
- }
122
-
123
- /* ─── Context Bar ──────────────────────────────────────────
124
- * Inspired by streetturtle/opencode-better-sidebar (context-progress)
125
- * Shows: progress bar, token usage / limit, session cost */
126
-
127
- function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
128
- const messages = createMemo((): any[] => {
129
- try { return (props.api.state.session as any).messages(props.sessionID) ?? [] }
130
- catch { return [] }
131
- })
132
-
133
- const usage = createMemo(() => {
134
- const msgs = messages()
135
- const lastAssistant = [...msgs].reverse().find((m: any) => {
136
- const role = m?.role ?? m?.info?.role
137
- return role === 'assistant' && safeNum(m?.tokens?.output) > 0
138
- })
139
- if (!lastAssistant) return null
140
-
141
- const tokens = msgTokens(lastAssistant)
142
- const pid = lastAssistant?.providerID ?? lastAssistant?.info?.providerID
143
- const mid = lastAssistant?.modelID ?? lastAssistant?.info?.modelID
144
- const provider = pid
145
- ? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
146
- : null
147
- const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
148
- const limit = ctxLimit > 0 ? ctxLimit : 200000
149
- return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
150
- })
151
-
152
- const totalCost = createMemo(() => {
153
- const state = ((props.api.state as any).session)?.get?.(props.sessionID)
154
- const fromState = msgCost(state)
155
- if (fromState > 0) return fromState
156
- return messages().reduce((sum: number, m: any) => sum + msgCost(m), 0)
157
- })
158
-
159
- const theme = () => props.api.theme.current
160
-
161
- return (
162
- <Show when={usage()}>
163
- {(u) => {
164
- const pct = u().percent
165
- const c = theme()
166
- const barColor = pct >= 90 ? c.error : pct >= 70 ? c.warning : c.accent
167
- const costStr = fmtCost(totalCost())
168
- return (
169
- <box flexDirection="column">
170
- <box flexDirection="row" gap={1}>
171
- <text fg={c.text} attributes={{ bold: true }}>Context</text>
172
- <text fg={barColor}>{buildBar(pct)}</text>
173
- <text fg={barColor}>{`${pct}%`}</text>
174
- </box>
175
- <text fg={c.textMuted}>
176
- {`${fmtInt(u().tokens)} / ${fmtInt(u().limit)} tok${costStr ? ` ${costStr}` : ''}`}
177
- </text>
178
- </box>
179
- )
180
- }}
181
- </Show>
182
- )
183
- }
184
-
185
- /* ─── Session Row (single session with live status indicator) ─ */
186
-
187
- function SessionRow(props: { api: TuiPluginApi; session: any }) {
188
- const theme = () => props.api.theme.current
189
-
190
- const status = createMemo(() => {
191
- try {
192
- const s = (props.api.state as any).session?.status?.(props.session.id)
193
- if (s?.type) return s.type as string
194
- } catch { /* ignore */ }
195
- return null
196
- })
197
-
198
- const statusIcon = createMemo(() => {
199
- switch (status()) {
200
- case 'busy': return '\u25cf ' // ● — running
201
- case 'retry': return '\u26a0 ' // ⚠ — needs attention
202
- default: return '' // idle/unknown — no icon
203
- }
204
- })
205
-
206
- return (
207
- <text fg={theme().textMuted}>
208
- {`${statusIcon()}${props.session?.id?.slice(0, 8) ?? '????'}... ${props.session?.title?.slice(0, 26) ?? '(untitled)'}`}
209
- </text>
210
- )
211
- }
212
-
213
- /* ─── Main Sidebar View ──────────────────────────────────── */
214
-
215
- function View(props: { api: TuiPluginApi; sessionID: string; version: string | null }) {
216
- const [showCommands, setShowCommands] = createSignal(false)
217
- const [showAgents, setShowAgents] = createSignal(false)
218
- const [showConfig, setShowConfig] = createSignal(false)
219
- const [showSessions, setShowSessions] = createSignal(false)
220
-
221
- const theme = () => props.api.theme.current
222
-
223
- const branch = createMemo(() =>
224
- props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
225
- )
226
-
227
- // ── Sessions: fetch via api.client.session.list (not proc.exec) ──
228
- const [sessionList, { refetch: refetchSessions }] = createResource(async () => {
229
- try {
230
- const result = await (props.api.client as any)?.session?.list?.({ limit: 100 })
231
- return result ?? { data: [] }
232
- } catch {
233
- return { data: [] }
234
- }
235
- })
236
-
237
- // Total top-level sessions (excludes subagent sessions)
238
- const totalSessions = createMemo(() => {
239
- const result = sessionList()
240
- if (!result) return 0
241
- const data: any[] = (result as any).data ?? result
242
- if (!Array.isArray(data)) return 0
243
- return data.filter((s: any) => !s.parentID).length
244
- })
245
-
246
- // Recent top-level sessions, sorted by time.updated descending
247
- const recentSessions = createMemo(() => {
248
- const result = sessionList()
249
- if (!result) return []
250
- const data: any[] = (result as any).data ?? result
251
- if (!Array.isArray(data)) return []
252
- return data
253
- .filter((s: any) => !s.parentID && s.id !== props.sessionID)
254
- .sort((a: any, b: any) => {
255
- const ta = a.time?.updated ?? a.updated ?? 0
256
- const tb = b.time?.updated ?? b.updated ?? 0
257
- return tb - ta
258
- })
259
- .slice(0, 8)
260
- })
261
-
262
- // ── Memory: starts at null (= "N/A"), tracks via events ──
263
- const [memoryCount, setMemoryCount] = createSignal<number | null>(null)
264
-
265
- // ── Event subscriptions for live session/memory updates ──
266
- onMount(() => {
267
- const cleanup: (() => void)[] = []
268
-
269
- // Session events — triggers resource refetch
270
- try {
271
- cleanup.push(props.api.event.on('session.status', refetchSessions))
272
- cleanup.push(props.api.event.on('session.created', refetchSessions))
273
- cleanup.push(props.api.event.on('session.updated', refetchSessions))
274
- cleanup.push(props.api.event.on('session.deleted', refetchSessions))
275
- } catch { /* events API not available in this runtime */ }
276
-
277
- // Memory events — tracks running counter (no api.state.memory)
278
- try {
279
- const ev = (props.api.event as any)
280
- if (typeof ev?.on === 'function') {
281
- cleanup.push(
282
- ev.on('memory.created', () => setMemoryCount((c) => (c !== null ? c + 1 : 1))),
283
- )
284
- cleanup.push(
285
- ev.on('memory.deleted', () => setMemoryCount((c) => (c !== null ? Math.max(0, c - 1) : 0))),
286
- )
287
- }
288
- } catch { /* memory events not available */ }
289
-
290
- onCleanup(() => cleanup.forEach((fn) => fn()))
291
- })
292
-
293
- // ── Config summary ──
294
- const configSummary = createMemo(() => {
295
- const cfg = (props.api.state as any).config
296
- if (!cfg) return null
297
- return {
298
- mcpCount: cfg.mcp ? Object.keys(cfg.mcp).length : 0,
299
- pluginCount: Array.isArray(cfg.plugin) ? cfg.plugin.length : 0,
300
- autoCompaction: cfg.compaction?.auto === true,
301
- }
302
- })
303
-
304
- const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
305
-
306
- return (
307
- <box flexDirection="column" width="100%">
308
- {/* ── Header ── */}
309
- <text fg={theme().accent} attributes={{ bold: true }}>
310
- {`Pantheon${props.version ? ` v${props.version}` : ''}`}
311
- </text>
312
- <Show when={branch()}>
313
- {(b) => <text fg={theme().textMuted}>{b()}</text>}
314
- </Show>
315
-
316
- <HR />
317
-
318
- {/* ── Context Bar (always visible) ── */}
319
- <ContextBar api={props.api} sessionID={props.sessionID} />
320
-
321
- <HR />
322
-
323
- {/* ── Sessions ── */}
324
- <box onMouseDown={() => setShowSessions((x) => !x)}>
325
- <text fg={theme().text} attributes={{ bold: true }}>
326
- {`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
327
- </text>
328
- <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
329
- </box>
330
- <Show when={showSessions()}>
331
- <Show
332
- when={recentSessions().length > 0}
333
- fallback={<box marginLeft={1}><text fg={theme().textMuted}>No recent sessions</text></box>}
334
- >
335
- <box marginLeft={1} flexDirection="column">
336
- <For each={recentSessions()}>
337
- {(ses: any) => <SessionRow api={props.api} session={ses} />}
338
- </For>
339
- </box>
340
- </Show>
341
- </Show>
342
-
343
- {/* ── Commands ── */}
344
- <box onMouseDown={() => setShowCommands((x) => !x)}>
345
- <text fg={theme().text} attributes={{ bold: true }}>
346
- {`${showCommands() ? '\u25bc' : '\u25b6'} Commands`}
347
- </text>
348
- <text fg={theme().textMuted}>{` (${String(COMMANDS.length)})`}</text>
349
- </box>
350
- <Show when={showCommands()}>
351
- <box marginLeft={1} flexDirection="column">
352
- <For each={COMMANDS}>
353
- {(cmd) => (
354
- <box onMouseDown={(e) => {
355
- e.stopPropagation()
356
- try {
357
- const cmdApi = (props.api as any).command
358
- const cmdName = cmd.name.replace('/', '')
359
- if (cmdApi?.trigger?.(cmdName)) return
360
- } catch { /* fall through to toast */ }
361
- props.api.ui?.toast?.({ title: 'Command', message: `Type ${cmd.name} in chat` })
362
- }}>
363
- <text fg={cmd.name === '/pantheon' ? theme().accent : theme().textMuted}>
364
- {cmd.name}
365
- </text>
366
- <text fg={theme().textMuted}>{` \u2014 ${cmd.desc}`}</text>
367
- </box>
368
- )}
369
- </For>
370
- </box>
371
- </Show>
372
-
373
- {/* ── Agents ── */}
374
- <box onMouseDown={() => setShowAgents((x) => !x)}>
375
- <text fg={theme().text} attributes={{ bold: true }}>
376
- {`${showAgents() ? '\u25bc' : '\u25b6'} Agents`}
377
- </text>
378
- <text fg={theme().textMuted}>{` (${String(AGENTS.length)})`}</text>
379
- </box>
380
- <Show when={showAgents()}>
381
- <box marginLeft={1} flexDirection="column">
382
- <For each={AGENTS}>
383
- {(agent) => (
384
- <box>
385
- <text fg={agent.tier === 'premium' ? theme().accent : theme().textMuted}>
386
- {`${agent.tier === 'premium' ? '\u2726 ' : '\u00b7 '}${agent.name}`}
387
- </text>
388
- <text fg={theme().textMuted}>{` \u2014 ${agent.role}`}</text>
389
- </box>
390
- )}
391
- </For>
392
- </box>
393
- </Show>
394
-
395
- {/* ── Config ── */}
396
- <box onMouseDown={() => setShowConfig((x) => !x)}>
397
- <text fg={theme().text} attributes={{ bold: true }}>
398
- {`${showConfig() ? '\u25bc' : '\u25b6'} Config`}
399
- </text>
400
- </box>
401
- <Show when={showConfig()}>
402
- <Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
403
- {(cfg) => (
404
- <box marginLeft={1} flexDirection="column">
405
- <text fg={theme().textMuted}>{`MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
406
- <text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
407
- </box>
408
- )}
409
- </Show>
410
- </Show>
411
-
412
- {/* ── Memory (always visible summary) ── */}
413
- <box marginTop={1}>
414
- <text fg={theme().textMuted}>
415
- {memoryCount() !== null
416
- ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
- : 'Memory: N/A'}
418
- </text>
419
- </box>
420
- </box>
421
- )
422
- }
423
-
424
- /* ─── Plugin Registration ────────────────────────────────── */
425
-
426
- const tui: TuiPlugin = async (api, _options, _meta) => {
427
- const version = await detectVersion(api)
428
-
429
- api.slots.register({
430
- order: 900,
431
- slots: {
432
- sidebar_content(_ctx, props) {
433
- return <View api={api} sessionID={props.session_id} version={version} />
434
- },
435
- },
436
- })
437
- }
438
-
439
- const plugin: TuiPluginModule & { id: string } = {
440
- id: 'pantheon.tui',
441
- tui,
442
- }
443
-
444
- export default plugin