pantheon-opencode 1.4.2 → 1.5.0-beta.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/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Validate the immutable inputs used by the GitHub Release → Zenodo workflow. */
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
import { compareVersions } from './version-check.mjs'
|
|
8
|
+
|
|
9
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
10
|
+
|
|
11
|
+
function validateHttpsUrl(value, name) {
|
|
12
|
+
let url
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(value)
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`${name} must be an absolute HTTPS URL.`)
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
url.protocol !== 'https:' ||
|
|
20
|
+
!url.hostname ||
|
|
21
|
+
url.username ||
|
|
22
|
+
url.password ||
|
|
23
|
+
/%(?![0-9A-Fa-f]{2})/.test(value) ||
|
|
24
|
+
/[\s{}\\|^`]/.test(value)
|
|
25
|
+
) {
|
|
26
|
+
throw new Error(`${name} must be an absolute HTTPS URL without credentials.`)
|
|
27
|
+
}
|
|
28
|
+
return url
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function maskHost(hostname) {
|
|
32
|
+
if (hostname.length <= 2) return '***'
|
|
33
|
+
return `${hostname[0]}***${hostname.at(-1)}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function summarizeZenodoConfiguration({
|
|
37
|
+
depositionsUrl,
|
|
38
|
+
filesUrlTemplate,
|
|
39
|
+
publishUrlTemplate,
|
|
40
|
+
creatorName,
|
|
41
|
+
}) {
|
|
42
|
+
const values = [
|
|
43
|
+
['ZENODO_DEPOSITIONS_URL', depositionsUrl, true],
|
|
44
|
+
['ZENODO_FILES_URL_TEMPLATE', filesUrlTemplate, true],
|
|
45
|
+
['ZENODO_PUBLISH_URL_TEMPLATE', publishUrlTemplate, true],
|
|
46
|
+
['ZENODO_CREATOR_NAME', creatorName, false],
|
|
47
|
+
]
|
|
48
|
+
return values.map(([name, value, isUrl]) => {
|
|
49
|
+
if (typeof value !== 'string' || !value) return { name, present: false }
|
|
50
|
+
if (!isUrl) return { name, present: true }
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(value.replaceAll('{id}', '1'))
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
present: true,
|
|
56
|
+
protocol: url.protocol,
|
|
57
|
+
host: maskHost(url.hostname),
|
|
58
|
+
hasWhitespace: /\s/.test(value),
|
|
59
|
+
hasCredentials: Boolean(url.username || url.password),
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
present: true,
|
|
65
|
+
protocol: 'invalid',
|
|
66
|
+
host: '***',
|
|
67
|
+
hasWhitespace: /\s/.test(value),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function zenodoTemplateUrl(template, id, name = 'Zenodo URL template') {
|
|
74
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Zenodo deposition id is invalid.')
|
|
75
|
+
if (typeof template !== 'string' || !template.includes('{id}'))
|
|
76
|
+
throw new Error(`${name} must contain the {id} placeholder.`)
|
|
77
|
+
return validateHttpsUrl(template.replaceAll('{id}', String(id)), name).href
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function zenodoDepositionUrl(depositionsUrl, id) {
|
|
81
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Zenodo deposition id is invalid.')
|
|
82
|
+
if (typeof depositionsUrl !== 'string')
|
|
83
|
+
throw new Error('ZENODO_DEPOSITIONS_URL must be an absolute HTTPS URL.')
|
|
84
|
+
const base = validateHttpsUrl(depositionsUrl, 'ZENODO_DEPOSITIONS_URL')
|
|
85
|
+
if (base.search || base.hash)
|
|
86
|
+
throw new Error('ZENODO_DEPOSITIONS_URL must not contain a query or fragment.')
|
|
87
|
+
return new URL(`${base.pathname.replace(/\/$/, '')}/${id}`, base.origin).href
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateZenodoConfiguration({
|
|
91
|
+
depositionsUrl,
|
|
92
|
+
filesUrlTemplate,
|
|
93
|
+
publishUrlTemplate,
|
|
94
|
+
creatorName,
|
|
95
|
+
}) {
|
|
96
|
+
const base = validateHttpsUrl(depositionsUrl, 'ZENODO_DEPOSITIONS_URL')
|
|
97
|
+
for (const [name, template] of [
|
|
98
|
+
['ZENODO_FILES_URL_TEMPLATE', filesUrlTemplate],
|
|
99
|
+
['ZENODO_PUBLISH_URL_TEMPLATE', publishUrlTemplate],
|
|
100
|
+
]) {
|
|
101
|
+
if (
|
|
102
|
+
typeof template !== 'string' ||
|
|
103
|
+
!template.includes('{id}') ||
|
|
104
|
+
template.replaceAll('{id}', '').match(/[{}]/) ||
|
|
105
|
+
template.includes('{{') ||
|
|
106
|
+
template.includes('}}')
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(`${name} must be an HTTPS URL template containing the {id} placeholder.`)
|
|
109
|
+
}
|
|
110
|
+
const url = new URL(zenodoTemplateUrl(template, 1, name))
|
|
111
|
+
if (url.origin !== base.origin) throw new Error(`${name} must use the Zenodo depositions host.`)
|
|
112
|
+
}
|
|
113
|
+
if (typeof creatorName !== 'string' || !creatorName.trim())
|
|
114
|
+
throw new Error('ZENODO_CREATOR_NAME must not be empty.')
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateZenodoRelease(root, tag) {
|
|
119
|
+
if (!/^v/.test(tag)) throw new Error('Release tag must start with v.')
|
|
120
|
+
const version = tag.slice(1)
|
|
121
|
+
if (!SEMVER.test(version)) throw new Error('Release tag is not valid semver.')
|
|
122
|
+
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
123
|
+
if (packageJson.name !== 'pantheon-opencode' || packageJson.version !== version) {
|
|
124
|
+
throw new Error('Release tag does not match package.json.')
|
|
125
|
+
}
|
|
126
|
+
const manifests = compareVersions(root)
|
|
127
|
+
if (!manifests.ok) throw new Error('Release manifests are out of sync.')
|
|
128
|
+
const citationPath = join(root, 'CITATION.cff')
|
|
129
|
+
const citation = { present: existsSync(citationPath), version: null }
|
|
130
|
+
if (citation.present) {
|
|
131
|
+
const match = readFileSync(citationPath, 'utf8').match(/^version:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
132
|
+
if (!match) throw new Error('CITATION.cff is missing its version field.')
|
|
133
|
+
citation.version = match[1]
|
|
134
|
+
if (citation.version !== version)
|
|
135
|
+
throw new Error('CITATION.cff version differs from the release tag.')
|
|
136
|
+
}
|
|
137
|
+
return { tag, version, citation }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
141
|
+
if (isDirectRun) {
|
|
142
|
+
try {
|
|
143
|
+
validateZenodoConfiguration({
|
|
144
|
+
depositionsUrl: process.env.ZENODO_DEPOSITIONS_URL,
|
|
145
|
+
filesUrlTemplate: process.env.ZENODO_FILES_URL_TEMPLATE,
|
|
146
|
+
publishUrlTemplate: process.env.ZENODO_PUBLISH_URL_TEMPLATE,
|
|
147
|
+
creatorName: process.env.ZENODO_CREATOR_NAME,
|
|
148
|
+
})
|
|
149
|
+
console.log(JSON.stringify(validateZenodoRelease(process.cwd(), process.env.RELEASE_TAG ?? '')))
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`zenodo validation: ${error.message}`)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
export function sha256File(path) {
|
|
5
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function findZenodoFile(record, filename, checksum) {
|
|
9
|
+
if (!record || typeof record !== 'object' || !Array.isArray(record.files)) {
|
|
10
|
+
throw new Error('Zenodo record did not contain a files array.')
|
|
11
|
+
}
|
|
12
|
+
const matches = record.files.filter((file) => file?.filename === filename)
|
|
13
|
+
if (matches.length > 1) throw new Error(`Zenodo record contains duplicate file: ${filename}.`)
|
|
14
|
+
if (matches.length === 0) return null
|
|
15
|
+
const file = matches[0]
|
|
16
|
+
const expected = `sha256:${checksum}`
|
|
17
|
+
if (file.checksum !== expected) {
|
|
18
|
+
throw new Error(`Zenodo checksum mismatch for ${filename}.`)
|
|
19
|
+
}
|
|
20
|
+
return file
|
|
21
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const DOI_PATTERN = /^10\.\d{4,9}\/\S+$/
|
|
2
|
+
const MARKER_PATTERN =
|
|
3
|
+
/<!-- zenodo-deposition:(?:id=(\d+);)?(?:doi=([^;\s]+);)?state=(pending|created|published)(?:;claim=([^\s]+))? -->/
|
|
4
|
+
|
|
5
|
+
export function extractZenodoDoi(response) {
|
|
6
|
+
if (!response || typeof response !== 'object') return null
|
|
7
|
+
const candidates = [response.doi, response.metadata?.doi, response.links?.doi]
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
if (typeof candidate === 'string' && DOI_PATTERN.test(candidate.trim())) return candidate.trim()
|
|
10
|
+
}
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateZenodoResponse(response, { requireDoi = false } = {}) {
|
|
15
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
16
|
+
throw new Error('Zenodo response must be a JSON object.')
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isSafeInteger(response.id) || response.id <= 0) {
|
|
19
|
+
throw new Error('Zenodo response did not contain a valid deposition id.')
|
|
20
|
+
}
|
|
21
|
+
const doi = extractZenodoDoi(response)
|
|
22
|
+
if (requireDoi && !doi) throw new Error('Zenodo response did not contain a valid DOI.')
|
|
23
|
+
return { id: response.id, doi }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseZenodoMarker(body) {
|
|
27
|
+
const text = String(body ?? '')
|
|
28
|
+
const match = text.match(MARKER_PATTERN)
|
|
29
|
+
if (!match) {
|
|
30
|
+
const legacy = text.match(/<!-- zenodo-deposition-id:(\d+) -->/)
|
|
31
|
+
if (!legacy) return null
|
|
32
|
+
const id = Number(legacy[1])
|
|
33
|
+
if (!Number.isSafeInteger(id) || id <= 0)
|
|
34
|
+
throw new Error('Zenodo marker has an invalid deposition id.')
|
|
35
|
+
return { id, doi: null, state: 'created', claim: null }
|
|
36
|
+
}
|
|
37
|
+
const id = match[1] ? Number(match[1]) : null
|
|
38
|
+
if (match[1] && (!Number.isSafeInteger(id) || id <= 0))
|
|
39
|
+
throw new Error('Zenodo marker has an invalid deposition id.')
|
|
40
|
+
if (match[2] && !DOI_PATTERN.test(match[2])) throw new Error('Zenodo marker has an invalid DOI.')
|
|
41
|
+
return { id, doi: match[2] ?? null, state: match[3], claim: match[4] ?? null }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function zenodoMarker({ id = null, doi = null, state, claim = null }) {
|
|
45
|
+
if (!['pending', 'created', 'published'].includes(state))
|
|
46
|
+
throw new Error('Zenodo marker state is invalid.')
|
|
47
|
+
if (id !== null && (!Number.isSafeInteger(id) || id <= 0))
|
|
48
|
+
throw new Error('Zenodo marker id is invalid.')
|
|
49
|
+
if (doi !== null && !DOI_PATTERN.test(doi)) throw new Error('Zenodo marker DOI is invalid.')
|
|
50
|
+
if (state !== 'pending' && id === null)
|
|
51
|
+
throw new Error('Created or published Zenodo markers require an id.')
|
|
52
|
+
return `<!-- zenodo-deposition:${id === null ? '' : `id=${id};`}${doi === null ? '' : `doi=${doi};`}state=${state}${claim === null ? '' : `;claim=${claim}`} -->`
|
|
53
|
+
}
|
|
@@ -34,11 +34,11 @@ def pantheon_home() -> Path:
|
|
|
34
34
|
"""
|
|
35
35
|
env = os.environ.get("PANTHEON_HOME")
|
|
36
36
|
if env:
|
|
37
|
-
return Path(env).expanduser().
|
|
37
|
+
return Path(env).expanduser().absolute()
|
|
38
38
|
|
|
39
39
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
40
40
|
if xdg:
|
|
41
|
-
return Path(xdg).expanduser().
|
|
41
|
+
return Path(xdg).expanduser().absolute() / "opencode"
|
|
42
42
|
|
|
43
43
|
return Path.home() / ".config" / "opencode"
|
|
44
44
|
|
|
@@ -48,7 +48,8 @@ def pantheon_project() -> Path | None:
|
|
|
48
48
|
|
|
49
49
|
Resolution priority:
|
|
50
50
|
1. $PANTHEON_PROJECT env var (explicit override)
|
|
51
|
-
2.
|
|
51
|
+
2. The validated logical ``$PWD`` supplied by the launcher
|
|
52
|
+
3. Current working directory (set by MCP client's cwd in opencode.json)
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
55
|
Absolute Path to the project root, or None if neither is available
|
|
@@ -56,10 +57,50 @@ def pantheon_project() -> Path | None:
|
|
|
56
57
|
"""
|
|
57
58
|
env = os.environ.get("PANTHEON_PROJECT")
|
|
58
59
|
if env:
|
|
59
|
-
return Path(env).expanduser().
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
return Path(env).expanduser().absolute()
|
|
61
|
+
|
|
62
|
+
logical_cwd = os.environ.get("PWD")
|
|
63
|
+
try:
|
|
64
|
+
cwd = os.getcwd()
|
|
65
|
+
except OSError:
|
|
66
|
+
cwd = ""
|
|
67
|
+
if logical_cwd and _is_valid_logical_cwd(logical_cwd, cwd):
|
|
68
|
+
return Path(logical_cwd).expanduser().absolute()
|
|
62
69
|
if cwd:
|
|
63
|
-
return Path(cwd).
|
|
70
|
+
return Path(cwd).absolute()
|
|
64
71
|
|
|
65
72
|
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_valid_logical_cwd(logical_cwd: str, physical_cwd: str) -> bool:
|
|
76
|
+
"""Validate a launcher-provided logical cwd without resolving its result.
|
|
77
|
+
|
|
78
|
+
``PWD`` is trusted only when it is an existing directory naming the same
|
|
79
|
+
inode as the process cwd. This preserves symlink spelling while rejecting
|
|
80
|
+
forged or stale environment values.
|
|
81
|
+
"""
|
|
82
|
+
logical = Path(logical_cwd).expanduser()
|
|
83
|
+
if not logical.is_absolute() or not logical.is_dir():
|
|
84
|
+
return False
|
|
85
|
+
try:
|
|
86
|
+
return os.path.samefile(logical, physical_cwd)
|
|
87
|
+
except (OSError, ValueError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def has_symlink_component(path: Path) -> bool:
|
|
92
|
+
"""Return whether any existing lexical component of ``path`` is a symlink.
|
|
93
|
+
|
|
94
|
+
The lexical path is intentionally inspected before ``resolve()`` so a
|
|
95
|
+
symlink cannot be hidden by resolving to an otherwise trusted directory.
|
|
96
|
+
Missing trailing components are allowed; this keeps normal project paths
|
|
97
|
+
usable while still checking every existing ancestor.
|
|
98
|
+
"""
|
|
99
|
+
current = Path(path.anchor) if path.is_absolute() else Path.cwd()
|
|
100
|
+
for component in path.parts[1:] if path.is_absolute() else path.parts:
|
|
101
|
+
if component in {"", "."}:
|
|
102
|
+
continue
|
|
103
|
+
current /= component
|
|
104
|
+
if current.is_symlink():
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
@@ -18,8 +18,8 @@ from __future__ import annotations
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
|
-
import signal
|
|
22
21
|
import shutil
|
|
22
|
+
import signal
|
|
23
23
|
import stat
|
|
24
24
|
import sys
|
|
25
25
|
import time
|
|
@@ -28,7 +28,6 @@ from pathlib import Path
|
|
|
28
28
|
from typing import Any
|
|
29
29
|
|
|
30
30
|
import yaml
|
|
31
|
-
|
|
32
31
|
from _pantheon_paths import pantheon_home, pantheon_project
|
|
33
32
|
from mcp.server.fastmcp import FastMCP
|
|
34
33
|
|
|
@@ -133,6 +132,34 @@ def _detect_prlimit() -> str | None:
|
|
|
133
132
|
# 1. /.opencode/.pantheon/code-mode/ (project install)
|
|
134
133
|
# 2. /.pantheon/code-mode/ (legacy fallback)
|
|
135
134
|
# 3. /.pantheon/code-mode/ (global fallback)
|
|
135
|
+
# 4. .pantheon/code-mode/ shipped inside the installed package (tarball
|
|
136
|
+
# fallback — package.json `files` includes .pantheon/code-mode/**)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _packaged_scripts_dir() -> Path | None:
|
|
140
|
+
"""Return the code-mode dir shipped inside the installed package, if any.
|
|
141
|
+
|
|
142
|
+
Walks up from this file looking for a `.pantheon/code-mode/` directory
|
|
143
|
+
(e.g. `<pkg>/.pantheon/code-mode` when installed from the tarball).
|
|
144
|
+
Returns None when no packaged copy exists.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
here = Path(__file__).resolve()
|
|
148
|
+
except Exception:
|
|
149
|
+
return None
|
|
150
|
+
for parent in [here.parent, *here.parents]:
|
|
151
|
+
try:
|
|
152
|
+
candidate = parent / ".pantheon" / "code-mode"
|
|
153
|
+
except Exception:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
if candidate.is_dir():
|
|
157
|
+
return candidate
|
|
158
|
+
except OSError:
|
|
159
|
+
continue
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
136
163
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
137
164
|
_SCRIPTS_DIR_CANDIDATES: list[Path] = []
|
|
138
165
|
_proj = pantheon_project()
|
|
@@ -146,6 +173,10 @@ for _candidate in _SCRIPTS_DIR_CANDIDATES:
|
|
|
146
173
|
if _candidate.is_dir():
|
|
147
174
|
SCRIPTS_DIR = _candidate
|
|
148
175
|
break
|
|
176
|
+
else:
|
|
177
|
+
_packaged = _packaged_scripts_dir()
|
|
178
|
+
if _packaged is not None:
|
|
179
|
+
SCRIPTS_DIR = _packaged
|
|
149
180
|
|
|
150
181
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
151
182
|
mcp = FastMCP(
|
package/src/mcp/eval_store.py
CHANGED
|
@@ -138,7 +138,7 @@ def store_eval(
|
|
|
138
138
|
except sqlite3.IntegrityError:
|
|
139
139
|
conn.rollback()
|
|
140
140
|
return {"error": f"Duplicate key or constraint violation: {key}"}
|
|
141
|
-
except Exception as e:
|
|
141
|
+
except Exception as e:
|
|
142
142
|
conn.rollback()
|
|
143
143
|
return {"error": f"Failed to store eval: {e}"}
|
|
144
144
|
finally:
|