pantheon-opencode 1.3.5 → 1.3.7
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 +33 -1
- package/package.json +3 -3
- package/scripts/_pantheon_paths.py +0 -1
- package/scripts/build-agents-md.mjs +4 -2
- package/scripts/code_mode_server.py +307 -21
- package/scripts/doctor.mjs +26 -13
- package/scripts/hooks/format-multi-language.sh +6 -3
- package/scripts/hooks/log-session-start.sh +6 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +22 -6
- package/scripts/hooks/validate-post-conditions.sh +1 -1
- package/scripts/hooks/validate-tool-safety.sh +5 -2
- package/scripts/install/agents-md.mjs +3 -1
- package/scripts/install/cli-ui.mjs +24 -4
- package/scripts/install/interactive.mjs +18 -7
- package/scripts/install/migrate-config.mjs +13 -19
- package/scripts/install/migrate.mjs +8 -5
- package/scripts/install/model-picker.mjs +4 -1
- package/scripts/install/opencode.mjs +2 -0
- package/scripts/install/shared.mjs +6 -4
- package/scripts/install/state.mjs +9 -6
- package/scripts/mcp_persistence_server.py +23 -22
- package/scripts/mcp_resources_server.py +60 -1
- package/scripts/memory_mcp_server.py +80 -4
- package/scripts/postinstall.mjs +27 -25
- package/scripts/prune.mjs +1 -1
- package/scripts/redaction-gate.mjs +3 -1
- package/scripts/sync-tui.mjs +148 -0
- package/scripts/uninstall.mjs +0 -2
- package/scripts/validation-policy.mjs +6 -1
- package/scripts/version-check.mjs +9 -4
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/demeter.md +1 -1
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/themis.md +4 -0
- package/src/mcp/code_mode_server.py +304 -18
- package/src/mcp/eval_store.py +233 -0
- package/src/mcp/mcp_persistence_server.py +23 -22
- package/src/mcp/mcp_resources_server.py +58 -0
- package/src/mcp/memory_mcp_server.py +80 -3
- package/src/pantheon/auto-wake.ts +2 -4
- package/src/pantheon/background-job-board.ts +17 -4
- package/src/pantheon/comment-checker.ts +48 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +21 -21
- package/src/pantheon/delegation-finalize.ts +44 -4
- package/src/pantheon/delegation-notify.ts +97 -0
- package/src/pantheon/delegation.ts +413 -23
- package/src/pantheon/dispatch-guard.ts +2 -0
- package/src/pantheon/file-persistence.ts +7 -3
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/permission-globs.ts +86 -0
- package/src/pantheon/presets.d.mts +41 -0
- package/src/pantheon/presets.mjs +134 -0
- package/src/pantheon/retry-policy.ts +212 -0
- package/src/pantheon/session-guard.ts +1 -3
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/tui-version-check.ts +72 -0
- package/src/pantheon/vision.ts +7 -5
- package/src/pantheon/zeus-delegate-with-retry.ts +81 -12
- package/src/plugin.ts +57 -3
- package/src/plugins/hook-runner.ts +1 -1
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +19 -14
- package/src/plugins/tui/dist/tui.tsx +52 -18
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +106 -24
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +42 -0
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
set -euo pipefail
|
|
25
25
|
|
|
26
26
|
# Project-local by default; set XDG_STATE_HOME for system-wide logging
|
|
27
|
+
# Anchor to PANTHEON_HOME or PANTHEON_PROJECT when available to avoid
|
|
28
|
+
# CWD-dependent relative path ("logs/agent-sessions" wrote to cwd).
|
|
27
29
|
if [ -n "${LOG_DIR:-}" ]; then
|
|
28
30
|
LOG_DIR="$LOG_DIR"
|
|
29
31
|
elif [ -n "${XDG_STATE_HOME:-}" ]; then
|
|
30
32
|
LOG_DIR="$XDG_STATE_HOME/pantheon/hooks"
|
|
33
|
+
elif [ -n "${PANTHEON_HOME:-}" ]; then
|
|
34
|
+
LOG_DIR="$PANTHEON_HOME/logs/agent-sessions"
|
|
35
|
+
elif [ -n "${PANTHEON_PROJECT:-}" ]; then
|
|
36
|
+
LOG_DIR="$PANTHEON_PROJECT/logs/agent-sessions"
|
|
31
37
|
else
|
|
32
38
|
LOG_DIR="logs/agent-sessions"
|
|
33
39
|
fi
|
|
@@ -133,20 +139,30 @@ echo "[DELEGATION] $AGENT_NAME started → $LOG_DIR/delegations.log" >&2
|
|
|
133
139
|
|
|
134
140
|
# --- Dispatch-time validation: check target against routing.yml ---
|
|
135
141
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
136
|
-
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." 2>/dev/null && pwd)"
|
|
143
|
+
|
|
144
|
+
# Find routing.yml: try repo root first, then PANTHEON_HOME, then PANTHEON_PROJECT.
|
|
145
|
+
# Silently skip if none found (deployed users may not have routing.yml locally).
|
|
146
|
+
ROUTING_FILE=""
|
|
147
|
+
if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/routing.yml" ]; then
|
|
148
|
+
ROUTING_FILE="$REPO_ROOT/routing.yml"
|
|
149
|
+
elif [ -n "${PANTHEON_HOME:-}" ] && [ -f "$PANTHEON_HOME/routing.yml" ]; then
|
|
150
|
+
ROUTING_FILE="$PANTHEON_HOME/routing.yml"
|
|
151
|
+
elif [ -n "${PANTHEON_PROJECT:-}" ] && [ -f "$PANTHEON_PROJECT/routing.yml" ]; then
|
|
152
|
+
ROUTING_FILE="$PANTHEON_PROJECT/routing.yml"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [ -n "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ] && [ "$AGENT_NAME" != "unknown" ]; then
|
|
139
156
|
ESCAPED_AGENT_NAME=$(printf '%s\n' "$AGENT_NAME" | sed 's/[][(){}.^$*+?|\/\\-]/\\&/g')
|
|
140
157
|
if grep -qE "^[[:space:]]*-[[:space:]]+target:[[:space:]]+$ESCAPED_AGENT_NAME[[:space:]]*$" "$ROUTING_FILE" 2>/dev/null; then
|
|
141
158
|
echo "[VALIDATION] ✅ Target '$AGENT_NAME' found in routing.yml" >&2
|
|
142
159
|
else
|
|
143
160
|
echo "[VALIDATION] ⚠️ Target '$AGENT_NAME' NOT in routing.yml delegation rules" >&2
|
|
144
161
|
fi
|
|
145
|
-
elif [ -
|
|
162
|
+
elif [ -n "$ROUTING_FILE" ]; then
|
|
146
163
|
echo "[VALIDATION] ⚠️ Could not identify the delegated agent (agent='$AGENT_NAME') — skipping routing.yml validation" >&2
|
|
147
|
-
elif [ ! -f "$ROUTING_FILE" ]; then
|
|
148
|
-
echo "[VALIDATION] ⚠️ Could not read routing.yml for validation" >&2
|
|
149
164
|
fi
|
|
165
|
+
# else: no routing.yml found — silently skip (deployed users may not have it locally)
|
|
150
166
|
|
|
151
167
|
# --- Cost estimation for delegation ---
|
|
152
168
|
declare -A TIER_MAP
|
|
@@ -9,7 +9,7 @@ SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.st
|
|
|
9
9
|
|
|
10
10
|
REVIEW_DIR=".pantheon/memory-bank/.tmp"
|
|
11
11
|
if [ -d "$REVIEW_DIR" ]; then
|
|
12
|
-
REVIEW_COUNT=$(
|
|
12
|
+
REVIEW_COUNT=$(find "$REVIEW_DIR" -maxdepth 1 -name 'REVIEW-*' -type f 2>/dev/null | wc -l)
|
|
13
13
|
if [ "$REVIEW_COUNT" -eq 0 ]; then
|
|
14
14
|
echo "[POST-CONDITION] ⚠️ No REVIEW artifact found in $REVIEW_DIR/" >&2
|
|
15
15
|
echo "[POST-CONDITION] 💡 Remember: implementation agents MUST call @themis for review" >&2
|
|
@@ -18,8 +18,11 @@ BLOCKED_PATTERNS=(
|
|
|
18
18
|
"DROP\s+DATABASE"
|
|
19
19
|
"TRUNCATE\s+TABLE"
|
|
20
20
|
"DELETE\s+FROM\s+.*;?\s*$"
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# DEAD PATTERN REMOVED: `rm\s+.*\s+--no-preserve-root` — redundant with
|
|
22
|
+
# pattern #1 which already blocks `rm -rf /` regardless of flags.
|
|
23
|
+
# DEAD PATTERN REMOVED: `:(){ :|:& };:` — fork bomb syntax never appears
|
|
24
|
+
# in real tool_input.command payloads (the entire stdin would need to be
|
|
25
|
+
# exactly this 13-char token with zero surrounding JSON/context).
|
|
23
26
|
">\s*/dev/\(sd[a-z]\|hd[a-z]\|disk[0-9]*\)"
|
|
24
27
|
"dd\s+if=.*of=/dev/"
|
|
25
28
|
"mkfs\."
|
|
@@ -76,7 +76,9 @@ export function readCanonicalAgents() {
|
|
|
76
76
|
if (!existsSync(AGENTS_DIR)) return []
|
|
77
77
|
|
|
78
78
|
const files = readdirSync(AGENTS_DIR)
|
|
79
|
-
.filter(
|
|
79
|
+
.filter(
|
|
80
|
+
(f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
|
|
81
|
+
)
|
|
80
82
|
.sort()
|
|
81
83
|
|
|
82
84
|
const agents = []
|
|
@@ -95,22 +95,42 @@ export function summaryTable(items) {
|
|
|
95
95
|
const bot = `\u2514${'\u2500'.repeat(nameWidth + 2)}\u2534${'\u2500'.repeat(statusWidth + 2)}\u2518`
|
|
96
96
|
|
|
97
97
|
console.log(tag(top))
|
|
98
|
-
console.log(
|
|
98
|
+
console.log(
|
|
99
|
+
tag(`\u2502 ${'Componente'.padEnd(nameWidth)} \u2502 ${'Status'.padEnd(statusWidth)} \u2502`),
|
|
100
|
+
)
|
|
99
101
|
console.log(tag(sep))
|
|
100
102
|
for (const item of items) {
|
|
101
|
-
console.log(
|
|
103
|
+
console.log(
|
|
104
|
+
tag(
|
|
105
|
+
`\u2502 ${item.name.padEnd(nameWidth)} \u2502 ${String(item.status).padEnd(statusWidth)} \u2502`,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
102
108
|
}
|
|
103
109
|
console.log(tag(bot))
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export function spinner(message) {
|
|
107
|
-
const frames = [
|
|
113
|
+
const frames = [
|
|
114
|
+
'\u280b',
|
|
115
|
+
'\u2819',
|
|
116
|
+
'\u2839',
|
|
117
|
+
'\u2838',
|
|
118
|
+
'\u283c',
|
|
119
|
+
'\u2834',
|
|
120
|
+
'\u2826',
|
|
121
|
+
'\u2827',
|
|
122
|
+
'\u2807',
|
|
123
|
+
'\u280f',
|
|
124
|
+
]
|
|
108
125
|
let i = 0
|
|
109
126
|
let running = true
|
|
110
127
|
|
|
111
128
|
if (tty() && !_quiet) {
|
|
112
129
|
const interval = setInterval(() => {
|
|
113
|
-
if (!running) {
|
|
130
|
+
if (!running) {
|
|
131
|
+
clearInterval(interval)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
114
134
|
process.stdout.write(`\r${frames[i % frames.length]} ${tag(message)}...`)
|
|
115
135
|
i++
|
|
116
136
|
}, 80)
|
|
@@ -31,7 +31,10 @@ function teardownStdin() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export async function promptMultiSelect(
|
|
34
|
+
export async function promptMultiSelect(
|
|
35
|
+
options,
|
|
36
|
+
{ title = '', selected = new Set(), pageSize = 6 } = {},
|
|
37
|
+
) {
|
|
35
38
|
if (options.length === 0) return new Set()
|
|
36
39
|
|
|
37
40
|
const preSelected = selected instanceof Set ? selected : new Set(selected)
|
|
@@ -83,7 +86,11 @@ export async function promptMultiSelect(options, { title = '', selected = new Se
|
|
|
83
86
|
const count = items.filter((i) => i.checked).length
|
|
84
87
|
const total = items.length
|
|
85
88
|
const noun = count === 1 ? 'selected' : 'selected'
|
|
86
|
-
lines.push(
|
|
89
|
+
lines.push(
|
|
90
|
+
colors.dim(
|
|
91
|
+
`(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
87
94
|
|
|
88
95
|
const lineCount = lines.length
|
|
89
96
|
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
@@ -247,7 +254,11 @@ export async function promptList(options, { title = '' } = {}) {
|
|
|
247
254
|
|
|
248
255
|
if (hasMoreBelow) lines.push(colors.dim(' ...'))
|
|
249
256
|
|
|
250
|
-
lines.push(
|
|
257
|
+
lines.push(
|
|
258
|
+
colors.dim(
|
|
259
|
+
`(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
251
262
|
|
|
252
263
|
const lineCount = lines.length
|
|
253
264
|
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
@@ -307,9 +318,7 @@ export async function promptList(options, { title = '' } = {}) {
|
|
|
307
318
|
}
|
|
308
319
|
|
|
309
320
|
export async function runInteractiveInstall({ components = [], defaultComponents = [] } = {}) {
|
|
310
|
-
const defaultSet = new Set(
|
|
311
|
-
defaultComponents.length > 0 ? defaultComponents : components,
|
|
312
|
-
)
|
|
321
|
+
const defaultSet = new Set(defaultComponents.length > 0 ? defaultComponents : components)
|
|
313
322
|
|
|
314
323
|
const componentOptions = [
|
|
315
324
|
{
|
|
@@ -362,7 +371,9 @@ export async function runInteractiveInstall({ components = [], defaultComponents
|
|
|
362
371
|
|
|
363
372
|
if (selectedComponents.size === 0) {
|
|
364
373
|
process.stdout.write(`${colors.yellow('No components selected — using defaults\n')}`)
|
|
365
|
-
defaultSet.forEach((v) =>
|
|
374
|
+
defaultSet.forEach((v) => {
|
|
375
|
+
selectedComponents.add(v)
|
|
376
|
+
})
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
const targetOptions = [
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
import {
|
|
19
|
-
readState,
|
|
20
|
-
writeState,
|
|
21
|
-
createInitialState,
|
|
22
|
-
addMigrationRecord,
|
|
23
|
-
updateVersion,
|
|
24
|
-
} from './state.mjs'
|
|
25
|
-
import {
|
|
26
|
-
readCheckpoint,
|
|
27
|
-
writeCheckpoint,
|
|
28
19
|
advanceCheckpoint,
|
|
20
|
+
listAppliedMigrations,
|
|
21
|
+
readCheckpoint,
|
|
29
22
|
removeCheckpoint,
|
|
30
23
|
resumePendingMigration,
|
|
31
|
-
|
|
24
|
+
writeCheckpoint,
|
|
32
25
|
} from './migrate.mjs'
|
|
26
|
+
import {
|
|
27
|
+
addMigrationRecord,
|
|
28
|
+
createInitialState,
|
|
29
|
+
readState,
|
|
30
|
+
updateVersion,
|
|
31
|
+
writeState,
|
|
32
|
+
} from './state.mjs'
|
|
33
33
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
// Migration step definitions
|
|
@@ -290,16 +290,10 @@ function main() {
|
|
|
290
290
|
addMigrationRecord(state, config.from, config.to, 'completed')
|
|
291
291
|
removeCheckpoint(config.target)
|
|
292
292
|
writeState(config.target, state)
|
|
293
|
-
console.log(
|
|
294
|
-
|
|
295
|
-
)
|
|
296
|
-
console.log(
|
|
297
|
-
` State written to: ${join(config.target, '.pantheon', 'install-state.json')}`,
|
|
298
|
-
)
|
|
293
|
+
console.log(`\n \u2705 Migration complete: ${config.from} \u2192 ${config.to}`)
|
|
294
|
+
console.log(` State written to: ${join(config.target, '.pantheon', 'install-state.json')}`)
|
|
299
295
|
} else {
|
|
300
|
-
console.log(
|
|
301
|
-
`\n \u2705 Migration would complete (dry-run): ${config.from} \u2192 ${config.to}`,
|
|
302
|
-
)
|
|
296
|
+
console.log(`\n \u2705 Migration would complete (dry-run): ${config.from} \u2192 ${config.to}`)
|
|
303
297
|
}
|
|
304
298
|
}
|
|
305
299
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { dirname, join } from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
|
-
import { readState, writeState
|
|
14
|
+
import { createInitialState, readState, writeState } from './state.mjs'
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
17
17
|
const ROOT = join(__dirname, '..', '..')
|
|
@@ -243,9 +243,7 @@ export function rollbackMigration(target, version) {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
const migrations = state.applied_migrations || []
|
|
246
|
-
const targetMigration = migrations.find(
|
|
247
|
-
(m) => m.to === version && m.status === 'completed',
|
|
248
|
-
)
|
|
246
|
+
const targetMigration = migrations.find((m) => m.to === version && m.status === 'completed')
|
|
249
247
|
if (!targetMigration) {
|
|
250
248
|
console.log(` \u26A0\uFE0F No completed migration found for version ${version}`)
|
|
251
249
|
return { success: false, reason: 'Migration not found' }
|
|
@@ -341,7 +339,12 @@ function main() {
|
|
|
341
339
|
console.log(` Previous version: ${history.previousVersion || 'N/A'}`)
|
|
342
340
|
console.log(` Applied migrations: ${history.migrations.length}`)
|
|
343
341
|
for (const m of history.migrations) {
|
|
344
|
-
const icon =
|
|
342
|
+
const icon =
|
|
343
|
+
m.status === 'completed'
|
|
344
|
+
? '\u2705'
|
|
345
|
+
: m.status === 'rolled_back'
|
|
346
|
+
? '\u{1F504}'
|
|
347
|
+
: '\u26A0\uFE0F'
|
|
345
348
|
console.log(` ${icon} ${m.from} \u2192 ${m.to} [${m.status}] (${m.applied_at})`)
|
|
346
349
|
}
|
|
347
350
|
return
|
|
@@ -55,7 +55,10 @@ export function writeActivePreset(
|
|
|
55
55
|
copyFileSync(filePath, backupPath)
|
|
56
56
|
backupMade = true
|
|
57
57
|
}
|
|
58
|
-
writeFileSync(
|
|
58
|
+
writeFileSync(
|
|
59
|
+
`${filePath}.tmp-${process.pid}`,
|
|
60
|
+
`${JSON.stringify(buildActivePresetFile(presetName, source), null, 2)}\n`,
|
|
61
|
+
)
|
|
59
62
|
renameSync(`${filePath}.tmp-${process.pid}`, filePath)
|
|
60
63
|
return { written: true, path: filePath, backupPath: backupMade ? backupPath : null }
|
|
61
64
|
}
|
|
@@ -452,6 +452,7 @@ export async function installOpenCode(
|
|
|
452
452
|
'_pantheon_paths.py',
|
|
453
453
|
'mcp_persistence_server.py',
|
|
454
454
|
'pantheon_vision_server.py',
|
|
455
|
+
'eval_store.py',
|
|
455
456
|
]
|
|
456
457
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
457
458
|
// Canonical MCP server sources live in src/mcp/. Map them explicitly so the
|
|
@@ -466,6 +467,7 @@ export async function installOpenCode(
|
|
|
466
467
|
'_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
467
468
|
'mcp_persistence_server.py': join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
|
|
468
469
|
'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
|
|
470
|
+
'eval_store.py': join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
469
471
|
}
|
|
470
472
|
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
471
473
|
if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
import { basename, dirname, join } from 'node:path'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
17
|
|
|
18
|
-
const REQUIRED_NODE_MAJOR = 18
|
|
18
|
+
const REQUIRED_NODE_MAJOR = 18
|
|
19
19
|
if (parseInt(process.versions.node.split('.')[0], 10) < REQUIRED_NODE_MAJOR) {
|
|
20
|
-
console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
|
|
21
|
-
process.exit(1)
|
|
20
|
+
console.error(`❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`)
|
|
21
|
+
process.exit(1)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
import yaml from 'js-yaml'
|
|
@@ -33,7 +33,9 @@ export const PLATFORM_DIR = join(ROOT, 'platform')
|
|
|
33
33
|
export function getAgentNames() {
|
|
34
34
|
if (!existsSync(AGENTS_DIR)) return []
|
|
35
35
|
return readdirSync(AGENTS_DIR)
|
|
36
|
-
.filter(
|
|
36
|
+
.filter(
|
|
37
|
+
(f) => (f.endsWith('.agent.md') || f.endsWith('.md')) && f.toLowerCase() !== 'readme.md',
|
|
38
|
+
)
|
|
37
39
|
.map((f) => f.replace(/\.(agent\.)?md$/, ''))
|
|
38
40
|
.sort()
|
|
39
41
|
}
|
|
@@ -95,12 +95,15 @@ export function upgradeStateV1toV2(state) {
|
|
|
95
95
|
updated_at: state.updated_at || now,
|
|
96
96
|
migration_checkpoint: null,
|
|
97
97
|
applied_migrations: Array.isArray(state.applied_migrations) ? state.applied_migrations : [],
|
|
98
|
-
components:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
components:
|
|
99
|
+
state.components && typeof state.components === 'object'
|
|
100
|
+
? state.components
|
|
101
|
+
: {
|
|
102
|
+
agents: { version: 1, status: 'installed' },
|
|
103
|
+
skills: { version: 1, status: 'installed' },
|
|
104
|
+
plugins: { version: 1, status: 'unknown' },
|
|
105
|
+
runtime: { version: 1, status: 'unknown' },
|
|
106
|
+
},
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -178,14 +178,15 @@ _global_root = (
|
|
|
178
178
|
_global_db = _init_db(_global_root / "global.db")
|
|
179
179
|
|
|
180
180
|
_project_db_instance = None
|
|
181
|
+
_project_db_path: Path | None = None
|
|
181
182
|
if args.project_db:
|
|
182
|
-
|
|
183
|
+
_project_db_path = Path(args.project_db)
|
|
184
|
+
_project_db_instance = _init_db(_project_db_path)
|
|
183
185
|
else:
|
|
184
186
|
_proj = pantheon_project()
|
|
185
187
|
if _proj:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
188
|
+
_project_db_path = _proj / ".pantheon" / "persistence" / "project.db"
|
|
189
|
+
_project_db_instance = _init_db(_project_db_path)
|
|
189
190
|
_project_db = _project_db_instance
|
|
190
191
|
|
|
191
192
|
|
|
@@ -262,7 +263,7 @@ async def kv_get(
|
|
|
262
263
|
row = conn.execute(
|
|
263
264
|
"SELECT value FROM kv_store "
|
|
264
265
|
"WHERE namespace = ? AND key = ? "
|
|
265
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
266
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
266
267
|
"AND deleted_at IS NULL",
|
|
267
268
|
(namespace, key),
|
|
268
269
|
).fetchone()
|
|
@@ -289,13 +290,13 @@ async def kv_stats(
|
|
|
289
290
|
total = conn.execute("SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL").fetchone()[0]
|
|
290
291
|
expired = conn.execute(
|
|
291
292
|
"SELECT COUNT(*) FROM kv_store "
|
|
292
|
-
"WHERE expires_at IS NOT NULL AND expires_at < datetime('now') "
|
|
293
|
+
"WHERE expires_at IS NOT NULL AND datetime(expires_at) < datetime('now') "
|
|
293
294
|
"AND deleted_at IS NULL"
|
|
294
295
|
).fetchone()[0]
|
|
295
296
|
|
|
296
297
|
ns_rows = conn.execute(
|
|
297
298
|
"SELECT namespace, COUNT(*) as cnt, "
|
|
298
|
-
"SUM(CASE WHEN expires_at IS NOT NULL AND expires_at < datetime('now') THEN 1 ELSE 0 END) as expired_count "
|
|
299
|
+
"SUM(CASE WHEN expires_at IS NOT NULL AND datetime(expires_at) < datetime('now') THEN 1 ELSE 0 END) as expired_count "
|
|
299
300
|
"FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
|
|
300
301
|
).fetchall()
|
|
301
302
|
|
|
@@ -372,7 +373,7 @@ async def kv_list(
|
|
|
372
373
|
rows = conn.execute(
|
|
373
374
|
"SELECT key, value, created_at, expires_at FROM kv_store "
|
|
374
375
|
"WHERE namespace = ? AND key LIKE ? "
|
|
375
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
376
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
376
377
|
"AND deleted_at IS NULL "
|
|
377
378
|
"ORDER BY key LIMIT ?",
|
|
378
379
|
(namespace, f"{prefix}%", limit),
|
|
@@ -426,7 +427,7 @@ async def kv_search(
|
|
|
426
427
|
"JOIN kv_store ON kv_store_fts.rowid = kv_store.id "
|
|
427
428
|
"WHERE kv_store_fts MATCH ? "
|
|
428
429
|
"AND kv_store.deleted_at IS NULL "
|
|
429
|
-
"AND (kv_store.expires_at IS NULL OR kv_store.expires_at > datetime('now'))"
|
|
430
|
+
"AND (kv_store.expires_at IS NULL OR datetime(kv_store.expires_at) > datetime('now'))"
|
|
430
431
|
)
|
|
431
432
|
params: list[str | int] = [fts_query]
|
|
432
433
|
|
|
@@ -474,7 +475,7 @@ async def purge_expired(
|
|
|
474
475
|
expired = conn.execute(
|
|
475
476
|
"SELECT key FROM kv_store "
|
|
476
477
|
"WHERE expires_at IS NOT NULL "
|
|
477
|
-
"AND expires_at < datetime('now') "
|
|
478
|
+
"AND datetime(expires_at) < datetime('now') "
|
|
478
479
|
"AND deleted_at IS NULL",
|
|
479
480
|
).fetchall()
|
|
480
481
|
expired_keys = [r[0] for r in expired]
|
|
@@ -490,7 +491,7 @@ async def purge_expired(
|
|
|
490
491
|
conn.execute(
|
|
491
492
|
"UPDATE kv_store SET deleted_at = datetime('now') "
|
|
492
493
|
"WHERE expires_at IS NOT NULL "
|
|
493
|
-
"AND expires_at < datetime('now') "
|
|
494
|
+
"AND datetime(expires_at) < datetime('now') "
|
|
494
495
|
"AND deleted_at IS NULL",
|
|
495
496
|
)
|
|
496
497
|
conn.commit()
|
|
@@ -516,7 +517,7 @@ def _opportunistic_auto_purge(conn: sqlite3.Connection, namespace: str, threshol
|
|
|
516
517
|
conn.execute(
|
|
517
518
|
"UPDATE kv_store SET deleted_at = datetime('now') "
|
|
518
519
|
"WHERE namespace = ? AND expires_at IS NOT NULL "
|
|
519
|
-
"AND expires_at < datetime('now') AND deleted_at IS NULL",
|
|
520
|
+
"AND datetime(expires_at) < datetime('now') AND deleted_at IS NULL",
|
|
520
521
|
(namespace,),
|
|
521
522
|
)
|
|
522
523
|
conn.commit()
|
|
@@ -659,7 +660,7 @@ async def context_get(
|
|
|
659
660
|
row = conn.execute(
|
|
660
661
|
"SELECT value FROM kv_store "
|
|
661
662
|
"WHERE namespace = ? AND key = ? "
|
|
662
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
663
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
663
664
|
"AND deleted_at IS NULL",
|
|
664
665
|
(ns, key),
|
|
665
666
|
).fetchone()
|
|
@@ -691,7 +692,7 @@ async def context_list(
|
|
|
691
692
|
rows = conn.execute(
|
|
692
693
|
"SELECT key, created_at, expires_at FROM kv_store "
|
|
693
694
|
"WHERE namespace = ? "
|
|
694
|
-
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
695
|
+
"AND (expires_at IS NULL OR datetime(expires_at) > datetime('now')) "
|
|
695
696
|
"AND deleted_at IS NULL "
|
|
696
697
|
"ORDER BY created_at DESC LIMIT 50",
|
|
697
698
|
(ns,),
|
|
@@ -737,7 +738,7 @@ async def context_stats(
|
|
|
737
738
|
|
|
738
739
|
expired = conn.execute(
|
|
739
740
|
"SELECT COUNT(*) FROM kv_store "
|
|
740
|
-
"WHERE namespace = ? AND expires_at < datetime('now') "
|
|
741
|
+
"WHERE namespace = ? AND datetime(expires_at) < datetime('now') "
|
|
741
742
|
"AND deleted_at IS NULL",
|
|
742
743
|
(ns,),
|
|
743
744
|
).fetchone()[0]
|
|
@@ -773,15 +774,15 @@ async def context_stats(
|
|
|
773
774
|
}
|
|
774
775
|
|
|
775
776
|
def _resolve_db_path(scope: str) -> Path | None:
|
|
776
|
-
"""Resolve the database file path for a given scope.
|
|
777
|
+
"""Resolve the database file path for a given scope.
|
|
778
|
+
|
|
779
|
+
Returns the actual path used at init (honoring an explicit
|
|
780
|
+
``--project-db``), so deletelog writes and db_size stats land next to
|
|
781
|
+
the real database file.
|
|
782
|
+
"""
|
|
777
783
|
if scope == "global":
|
|
778
784
|
return _global_root / "global.db"
|
|
779
|
-
|
|
780
|
-
# Find the project root from the project DB path
|
|
781
|
-
proj_root = pantheon_project()
|
|
782
|
-
if proj_root:
|
|
783
|
-
return proj_root / ".pantheon" / "persistence" / "project.db"
|
|
784
|
-
return None
|
|
785
|
+
return _project_db_path
|
|
785
786
|
|
|
786
787
|
|
|
787
788
|
# ── Main Entrypoint ─────────────────────────────────────────────────────────────
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Auto-generated: resolved symlink from ../src/mcp/mcp_resources_server.py
|
|
2
1
|
#!/usr/bin/env python3
|
|
3
2
|
"""Pantheon MCP Resources Server.
|
|
4
3
|
|
|
@@ -19,6 +18,7 @@ from typing import Any
|
|
|
19
18
|
|
|
20
19
|
import yaml
|
|
21
20
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
21
|
+
from eval_store import get_latest_eval, list_evals
|
|
22
22
|
from mcp.server.fastmcp import FastMCP
|
|
23
23
|
|
|
24
24
|
# ── Path Resolution ──────────────────────────────────────────────────────────
|
|
@@ -112,6 +112,8 @@ async def list_skills() -> str:
|
|
|
112
112
|
async def get_routing() -> str:
|
|
113
113
|
"""Return the full content of routing.yml."""
|
|
114
114
|
routing_file = _PANTHEON_HOME / "routing.yml"
|
|
115
|
+
if not routing_file.exists() and _PANTHEON_PROJECT:
|
|
116
|
+
routing_file = _PANTHEON_PROJECT / "src" / "routing.yml"
|
|
115
117
|
if not routing_file.exists():
|
|
116
118
|
return "routing.yml not found."
|
|
117
119
|
return routing_file.read_text(encoding="utf-8")
|
|
@@ -200,6 +202,63 @@ async def get_memory_bank(path: str) -> str:
|
|
|
200
202
|
return resolved.read_text(encoding="utf-8")
|
|
201
203
|
|
|
202
204
|
|
|
205
|
+
# ── Plugin Eval Resources ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _valid_plugin_name(name: str) -> bool:
|
|
209
|
+
"""Validate a plugin name for the eval resource (no traversal)."""
|
|
210
|
+
return bool(name) and "/" not in name and "\\" not in name and ".." not in name
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.resource(
|
|
214
|
+
"pantheon://eval",
|
|
215
|
+
description="List of evaluated plugins/skills with their latest "
|
|
216
|
+
"certification score (from the plugin_eval memory namespace)",
|
|
217
|
+
)
|
|
218
|
+
async def list_plugin_evals() -> str:
|
|
219
|
+
"""Return a markdown list of evaluated plugins with latest scores."""
|
|
220
|
+
entries = list_evals()
|
|
221
|
+
if not entries:
|
|
222
|
+
return "No plugin evals recorded yet."
|
|
223
|
+
|
|
224
|
+
lines = ["# Plugin Eval Certification", ""]
|
|
225
|
+
for entry in entries:
|
|
226
|
+
name = entry.get("name", "?")
|
|
227
|
+
score = entry.get("metadata", {}).get("score", "?")
|
|
228
|
+
created = entry.get("created_at", "")
|
|
229
|
+
lines.append(f"- **{name}** — score {score} (evaluated {created})")
|
|
230
|
+
return "\n".join(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@mcp.resource(
|
|
234
|
+
"pantheon://eval/{plugin}",
|
|
235
|
+
description="Latest certification report for a plugin/skill "
|
|
236
|
+
"(from the plugin_eval memory namespace)",
|
|
237
|
+
)
|
|
238
|
+
async def get_plugin_eval(plugin: str) -> str:
|
|
239
|
+
"""Return the latest eval certification report for a plugin.
|
|
240
|
+
|
|
241
|
+
Security: plugin names containing path separators or traversal
|
|
242
|
+
sequences are rejected before any lookup.
|
|
243
|
+
"""
|
|
244
|
+
if not _valid_plugin_name(plugin):
|
|
245
|
+
return "Invalid plugin name: blocked."
|
|
246
|
+
|
|
247
|
+
entry = get_latest_eval(plugin)
|
|
248
|
+
if entry is None:
|
|
249
|
+
return f"No eval found for plugin '{plugin}'."
|
|
250
|
+
|
|
251
|
+
score = entry.get("metadata", {}).get("score", "?")
|
|
252
|
+
created = entry.get("created_at", "")
|
|
253
|
+
value = entry.get("value", "{}")
|
|
254
|
+
return (
|
|
255
|
+
f"# Eval Report: {plugin}\n\n"
|
|
256
|
+
f"**Score:** {score}\n"
|
|
257
|
+
f"**Evaluated:** {created}\n\n"
|
|
258
|
+
f"```json\n{value}\n```"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
203
262
|
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
204
263
|
|
|
205
264
|
if __name__ == "__main__":
|