pantheon-opencode 1.6.0-beta.3 → 1.6.0-beta.5

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 (55) hide show
  1. package/AGENTS.md +24 -5
  2. package/CHANGELOG.md +49 -0
  3. package/README.md +51 -19
  4. package/README.pt-BR.md +32 -13
  5. package/bin/pantheon-init.mjs +26 -3
  6. package/package.json +12 -11
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +6 -6
  9. package/scripts/{code_mode_server.py → code_mode.py} +3 -3
  10. package/scripts/cost.mjs +244 -41
  11. package/scripts/doctor.mjs +151 -1
  12. package/scripts/install/agents-md.mjs +2 -2
  13. package/scripts/install/config-migration.mjs +80 -2
  14. package/scripts/install/health-check.mjs +21 -19
  15. package/scripts/install/opencode-version.mjs +246 -4
  16. package/scripts/install/opencode.mjs +250 -119
  17. package/scripts/install/shared.mjs +5 -5
  18. package/scripts/install-mcp.mjs +28 -9
  19. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
  20. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  21. package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
  22. package/scripts/probe-context-rehydrate.mjs +8 -2
  23. package/scripts/probe-pantheon-cost.mjs +71 -15
  24. package/scripts/sync-artifacts.mjs +5 -5
  25. package/scripts/test-opencode-v2-sandbox.sh +21 -21
  26. package/scripts/validate-routing.mjs +3 -3
  27. package/skills-lock.json +1 -1
  28. package/src/agents/apollo.md +7 -2
  29. package/src/agents/zeus.md +29 -1
  30. package/src/instructions/memory-protocol.instructions.md +15 -4
  31. package/src/instructions/zeus-anti-stall.instructions.md +9 -1
  32. package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
  33. package/src/mcp/eval_store.py +2 -2
  34. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
  35. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  36. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
  37. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +1 -1
  38. package/src/mcp/requirements-mcp.txt +6 -4
  39. package/src/mcp/requirements-vision.txt +2 -2
  40. package/src/pantheon/cost-command.ts +399 -101
  41. package/src/pantheon/presets.mjs +3 -3
  42. package/src/pantheon/v2-unsupported.d.mts +11 -0
  43. package/src/pantheon/v2-unsupported.mjs +113 -0
  44. package/src/plugin-v2.ts +269 -100
  45. package/src/plugins/tui/dist/server.d.ts +1 -2
  46. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  47. package/src/plugins/tui/dist/tui.d.ts +74 -74
  48. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  49. package/src/plugins/tui/dist/tui.js +17 -20
  50. package/src/plugins/tui/package-lock.json +364 -317
  51. package/src/plugins/tui/package.json +8 -5
  52. package/src/plugins/tui/tsconfig.build.json +5 -0
  53. package/src/plugins/tui/tsdown.config.ts +3 -0
  54. package/src/skills/context-compression/SKILL.md +1 -1
  55. package/opencode.json +0 -58
@@ -27,7 +27,7 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
27
27
  const visionServer = resolveVisionServer(target)
28
28
 
29
29
  // Check 1: Critical runtime scripts exist
30
- const scripts = ['mcp_persistence_server.py', '_pantheon_paths.py']
30
+ const scripts = ['mcp_persistence.py', '_pantheon_paths.py']
31
31
  for (const script of scripts) {
32
32
  const path = join(target, 'scripts', script)
33
33
  if (existsSync(path)) {
@@ -44,8 +44,8 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
44
44
  check: visionServer.check,
45
45
  detail: visionServer.detail,
46
46
  })
47
- const installedVisionServer = join(target, 'scripts', 'pantheon_vision_server.py')
48
- const canonicalVisionServer = join(target, 'src', 'mcp', 'pantheon_vision_server.py')
47
+ const installedVisionServer = join(target, 'scripts', 'pantheon_vision.py')
48
+ const canonicalVisionServer = join(target, 'src', 'mcp', 'pantheon_vision.py')
49
49
  if (existsSync(installedVisionServer) && existsSync(canonicalVisionServer)) {
50
50
  try {
51
51
  if (
@@ -54,7 +54,7 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
54
54
  ) {
55
55
  results.failed.push({
56
56
  check: 'pantheon-vision source drift',
57
- detail: 'installed script differs from src/mcp/pantheon_vision_server.py',
57
+ detail: 'installed script differs from src/mcp/pantheon_vision.py',
58
58
  })
59
59
  }
60
60
  } catch {
@@ -66,7 +66,7 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
66
66
  }
67
67
  } else {
68
68
  results.failed.push({
69
- check: 'src/mcp/pantheon_vision_server.py',
69
+ check: 'src/mcp/pantheon_vision.py',
70
70
  detail: 'NOT FOUND — canonical vision server source is unavailable',
71
71
  })
72
72
  }
@@ -104,12 +104,7 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
104
104
  }
105
105
 
106
106
  // Check 2: Syntax check on each MCP server script
107
- const mcpScripts = [
108
- 'mcp_persistence_server.py',
109
- 'mcp_resources_server.py',
110
- 'code_mode_server.py',
111
- 'memory_mcp_server.py',
112
- ]
107
+ const mcpScripts = ['mcp_persistence.py', 'mcp_resources.py', 'code_mode.py', 'memory_mcp.py']
113
108
  for (const script of mcpScripts) {
114
109
  const path = join(target, 'scripts', script)
115
110
  if (existsSync(path) && python) {
@@ -135,12 +130,12 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
135
130
  })
136
131
  if (result.status === 0) {
137
132
  results.passed.push({
138
- check: 'syntax:pantheon_vision_server.py',
133
+ check: 'syntax:pantheon_vision.py',
139
134
  detail: 'valid',
140
135
  })
141
136
  } else {
142
137
  results.failed.push({
143
- check: 'syntax:pantheon_vision_server.py',
138
+ check: 'syntax:pantheon_vision.py',
144
139
  detail: result.stderr.toString().trim().split('\n').slice(-1)[0],
145
140
  })
146
141
  }
@@ -173,13 +168,20 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
173
168
  if (python) {
174
169
  const mcpResult = spawnSync(
175
170
  python,
176
- ['-c', 'from mcp.server.fastmcp import FastMCP; print(FastMCP.__module__)'],
171
+ [
172
+ '-c',
173
+ 'import importlib.metadata as metadata; from fastmcp import FastMCP; ' +
174
+ 'print(f\'mcp={metadata.version("mcp")} fastmcp={metadata.version("fastmcp")} {FastMCP.__module__}\')',
175
+ ],
177
176
  { stdio: 'pipe' },
178
177
  )
179
178
  if (mcpResult.status === 0) {
180
179
  results.passed.push({ check: 'mcp-sdk', detail: mcpResult.stdout.toString().trim() })
181
180
  } else {
182
- results.failed.push({ check: 'mcp-sdk', detail: 'NOT INSTALLED — all MCP servers will fail' })
181
+ results.failed.push({
182
+ check: 'mcp-sdk',
183
+ detail: 'MCP SDK or standalone FastMCP is unavailable — all MCP servers will fail',
184
+ })
183
185
  }
184
186
  }
185
187
  // Check 4: the memory server's only search backend is the stdlib's own
@@ -224,13 +226,13 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
224
226
  function resolveVisionServer(target) {
225
227
  const candidates = [
226
228
  {
227
- path: join(target, 'src', 'mcp', 'pantheon_vision_server.py'),
228
- check: 'src/mcp/pantheon_vision_server.py',
229
+ path: join(target, 'src', 'mcp', 'pantheon_vision.py'),
230
+ check: 'src/mcp/pantheon_vision.py',
229
231
  detail: 'canonical source exists',
230
232
  },
231
233
  {
232
- path: join(target, 'scripts', 'pantheon_vision_server.py'),
233
- check: 'scripts/pantheon_vision_server.py',
234
+ path: join(target, 'scripts', 'pantheon_vision.py'),
235
+ check: 'scripts/pantheon_vision.py',
234
236
  detail: 'installed copy exists (deployed from src/mcp)',
235
237
  },
236
238
  ]
@@ -1,5 +1,9 @@
1
1
  /** OpenCode major-version selection shared by the CLI and installer. */
2
2
 
3
+ import { spawnSync } from 'node:child_process'
4
+
5
+ import { warning } from './cli-ui.mjs'
6
+
3
7
  const VALID_VERSIONS = new Set(['v1', 'v2', 'auto'])
4
8
 
5
9
  /** @param {string[]} args @returns {'v1'|'v2'|'auto'|null} */
@@ -28,12 +32,185 @@ function normalize(value) {
28
32
  }
29
33
 
30
34
  /**
31
- * Resolve auto conservatively: only an explicit hint selects V2.
35
+ * Ask the host binary for its version. Throws on any spawn failure so the
36
+ * caller can fail soft in one place.
37
+ *
38
+ * stderr is captured alongside stdout. A host that prints its version banner to
39
+ * stderr and nothing to stdout would otherwise soft-fail to V1 — a wrong
40
+ * answer whose cause has nothing to do with the host's generation.
41
+ * @param {string} binary
42
+ * @returns {{stdout: string, stderr: string}}
43
+ */
44
+ function probeHostVersion(binary) {
45
+ const result = spawnSync(binary, ['--version'], {
46
+ encoding: 'utf8',
47
+ timeout: 5000,
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ })
50
+ if (result.error) throw result.error
51
+ if (result.status !== 0) {
52
+ throw new Error(`"${binary} --version" exited with status ${result.status}`)
53
+ }
54
+ return { stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
55
+ }
56
+
57
+ // A version-SHAPED token: optional "v", a major, a dot, a minor. The lookbehind
58
+ // rejects starting mid-number so "1.18.33" offers ONE candidate ("1.18") rather
59
+ // than also offering "18.33". Case-insensitive because hosts print both
60
+ // "opencode v2.0.22" and "OpenCode V2.0.22".
61
+ const VERSION_TOKEN = /(?<![\d.])v?(\d+)\.(\d+)/gi
62
+
63
+ // A token immediately preceded by the tool name IS that host's own version,
64
+ // whatever else the banner mentions. This is what distinguishes
65
+ // "node v22.1.0 (opencode 1.18.33)" — where the two candidates are a runtime
66
+ // version and the tool version — from a genuine contradiction.
67
+ //
68
+ // KNOWN LIMIT, deliberately not worked around: this anchor is a NAME anchor, so
69
+ // any sentence that puts "opencode" in front of a version-shaped token reads as
70
+ // a host banner. A probe whose output is `warning: deprecated, upgrade to
71
+ // opencode 2.0.0` resolves to v2 in silence. Closing it would need a rule that
72
+ // rejects a mid-sentence "opencode X.Y.Z" — but "node v22.1.0 (opencode 1.18.33)"
73
+ // is exactly such a mid-sentence anchor, and it is the reason the anchor exists:
74
+ // dropping it lets a leading runtime token flip a genuine 1.x host to v2. The
75
+ // defect class is not the nag wording (a phrase blacklist closes these three
76
+ // strings and none of the others), it is name-adjacency. `probeHostVersion`
77
+ // spawns whatever OPENCODE_BIN names and accepts any exit-0 binary, so a wrapper
78
+ // or shim printing a bare path is indistinguishable from the real host by
79
+ // construction. An operator whose `opencode` is a wrapper gets v2 silently; the
80
+ // documented remedy is `--opencode-version`, not a heuristic that would trade
81
+ // this silent misclassification for a different one.
82
+ const ANCHOR_NAME = 'opencode'
83
+
84
+ // How far back the anchor scan looks. `[\s:=]*` is unbounded, so no finite
85
+ // window is exact — the cap is what makes the scan O(1) per token instead of
86
+ // copying the whole prefix, which was quadratic and let a verbose host outrun the
87
+ // 5s spawn timeout (that timeout guards the spawn, not the parse). The budget is
88
+ // sized from the anchor itself (ANCHOR_NAME plus separators), not guessed: the
89
+ // longest separator run in any realistic banner is ~10 bytes. Past the budget an
90
+ // anchor is simply missed, and that degrades to the loud consensus path — it can
91
+ // turn a quiet resolution into a warned one, never into a different resolution.
92
+ const ANCHOR_SEPARATOR_BUDGET = 56
93
+ const ANCHOR_WINDOW_BYTES = ANCHOR_NAME.length + ANCHOR_SEPARATOR_BUDGET
94
+ const TOOL_NAME_BEFORE = new RegExp(`${ANCHOR_NAME}[\\s:=]*$`, 'i')
95
+
96
+ // A major at or above this is a calendar year, not a release number:
97
+ // "built 2026.10.04" is version-SHAPED but describes no release. So is major 0,
98
+ // which is a prerelease placeholder rather than a release — and major 0 read as
99
+ // "older than 2" is the original defect (V1 config on a 2.x host) recurring for
100
+ // a 2.x beta. Which brings the subtlety this gate turns on:
101
+ //
102
+ // * UNANCHORED and implausible -> noise. A date or a build counter says
103
+ // nothing about the host's generation, so dropping it is a resolution, not a
104
+ // failure: "opencode v2.0.22 built 2026.10.04" stays quiet.
105
+ // * ANCHORED and implausible -> the host's OWN version, which we cannot read.
106
+ // Discarding it would promote a surviving `node v22` token to deciding power
107
+ // and answer in silence — "opencode 100.0.0 (node v22.1.0)" would resolve to
108
+ // v2 with no warning. So a refused anchored major is reported, not dropped.
109
+ //
110
+ // Both cases fail LOUD rather than silently, which is the safe direction, and
111
+ // the loud-v1 claim holds for the anchored case without depending on the dropped
112
+ // token being the only candidate.
113
+ const FIRST_PLAUSIBLE_MAJOR = 100
114
+
115
+ /** Is `major` a value we refuse to read as a release number? */
116
+ function isImplausibleMajor(major) {
117
+ return major === 0 || major >= FIRST_PLAUSIBLE_MAJOR
118
+ }
119
+
120
+ /**
121
+ * The slice of `source` the anchor scan inspects — bounded on purpose (see
122
+ * ANCHOR_WINDOW_BYTES) so the scan does not copy the prefix per token.
123
+ * @param {string} source @param {number} index @returns {string}
124
+ */
125
+ function anchorWindow(source, index) {
126
+ return source.slice(Math.max(0, index - ANCHOR_WINDOW_BYTES), index)
127
+ }
128
+
129
+ /**
130
+ * Reduce a `--version` string to the single major it unambiguously reports.
131
+ *
132
+ * Anchoring on the tool name — rather than taking the first or the last
133
+ * version-shaped token — is what makes this robust on both sides. First-match
134
+ * anchoring lets a leading `node v22` outrank the tool's own version and flips
135
+ * a 1.x host to v2; last-match anchoring lets a trailing `built 2026.10.04`
136
+ * outrank it and does the same. A name-anchored token defeats both.
137
+ *
138
+ * @param {string} text raw `--version` output from one stream
139
+ * @returns {{major: number}|{ambiguous: number[]}|{unreadable: number}|null}
140
+ * `{major}` when the host version is identifiable, `{ambiguous}` when
141
+ * version-shaped tokens disagree and nothing says which is the host's,
142
+ * `{unreadable}` when the host's own anchored token reports a major this gate
143
+ * will not interpret, and `null` when nothing is version-shaped.
144
+ */
145
+ function parseHostMajor(text) {
146
+ const source = String(text ?? '')
147
+ const candidates = []
148
+ let unreadable = null
149
+ for (const match of source.matchAll(VERSION_TOKEN)) {
150
+ const major = Number(match[1])
151
+ const anchored = TOOL_NAME_BEFORE.test(anchorWindow(source, match.index))
152
+ if (isImplausibleMajor(major)) {
153
+ if (anchored && unreadable === null) unreadable = major
154
+ continue
155
+ }
156
+ candidates.push({ major, anchored })
157
+ }
158
+ if (unreadable !== null) return { unreadable }
159
+ if (candidates.length === 0) return null
160
+
161
+ const anchored = candidates.filter((candidate) => candidate.anchored)
162
+ if (anchored.length > 0) {
163
+ const majors = new Set(anchored.map((candidate) => candidate.major))
164
+ if (majors.size === 1) return { major: anchored[0].major }
165
+ return { ambiguous: [...majors].sort((a, b) => a - b) }
166
+ }
167
+
168
+ const majors = new Set(candidates.map((candidate) => candidate.major))
169
+ if (majors.size === 1) return { major: candidates[0].major }
170
+ return { ambiguous: [...majors].sort((a, b) => a - b) }
171
+ }
172
+
173
+ /**
174
+ * Resolve the target OpenCode generation.
175
+ *
176
+ * `auto` precedence — the order is load-bearing, do not "simplify" it:
177
+ * 1. explicit `--opencode-version` argument
178
+ * 2. OPENCODE_VERSION env var
179
+ * 3. an `opencode2` binary basename
180
+ * 4. a `--version` probe of the host binary (major >= 2 => v2)
181
+ * 5. soft-fail to v1 with one visible warning
182
+ *
183
+ * Step 3 stays ahead of step 4 because of WHAT THE TWO STEPS ARE. Step 3 is an
184
+ * explicit operator hint: naming a binary `opencode2` and pointing
185
+ * `OPENCODE_BIN` at it is a deliberate act, and it is the only one of the two
186
+ * the operator gets to state. Step 4 is a heuristic read of a free-form string
187
+ * that some program chose to print. A hint must not be overruled by a
188
+ * heuristic — and the argument holds no matter what any given build prints.
189
+ *
190
+ * Historical note, NOT an observation of this host: an OpenCode 2.x prerelease
191
+ * once printed "0.0.0-next-17444", which any "major >= 2 => v2" probe reads as
192
+ * major 0 and would resolve to v1. That string does not come from the host —
193
+ * it survives only in this repo's comments and test fixtures. It is also why a
194
+ * major-0 anchored token now takes the loud path rather than resolving to v1 in
195
+ * silence (see FIRST_PLAUSIBLE_MAJOR). Verified 2026-10-04: `opencode2` here is
196
+ * a 47-byte shim that execs `opencode`, and `opencode2 --version` prints
197
+ * "opencode v2.0.22" on stdout. So for a real `opencode2` today both the
198
+ * basename and the probe return v2 and the ordering is harmless rather than
199
+ * load-bearing; what it still defends is the general case above.
200
+ *
201
+ * Step 5 falls back to v1 rather than v2 on purpose. Writing a plural
202
+ * `plugins` directory entry on an unknown 1.x host would be ignored there,
203
+ * losing the plugin on that host AND dropping the V1-only surface — strictly
204
+ * worse than the V1 default, which at least still works. The fallback is only
205
+ * tolerable because it warns; a silent v1 is what made this a silent failure.
206
+ *
32
207
  * @param {'v1'|'v2'|'auto'|null|undefined} requested
33
- * @param {{env?: Record<string, string|undefined>, binary?: string}} [options]
208
+ * @param {{env?: Record<string, string|undefined>, binary?: string,
209
+ * probe?: () => string|{stdout?: string, stderr?: string},
210
+ * warn?: (message: string) => void}} [options]
34
211
  * @returns {'v1'|'v2'}
35
212
  */
36
- export function resolveOpenCodeVersion(requested = 'v1', options = {}) {
213
+ export function resolveOpenCodeVersion(requested = 'auto', options = {}) {
37
214
  if (!VALID_VERSIONS.has(requested)) {
38
215
  throw new Error(`Invalid OpenCode version "${requested}" — expected v1, v2, or auto`)
39
216
  }
@@ -43,5 +220,70 @@ export function resolveOpenCodeVersion(requested = 'v1', options = {}) {
43
220
  return env.OPENCODE_VERSION
44
221
  }
45
222
  const binary = options.binary ?? env.OPENCODE_BIN ?? ''
46
- return /(?:^|[\\/])opencode2(?:\.exe)?$/i.test(binary) ? 'v2' : 'v1'
223
+ if (/(?:^|[\\/])opencode2(?:\.exe)?$/i.test(binary)) return 'v2'
224
+
225
+ const probe = options.probe ?? (() => probeHostVersion(binary || 'opencode'))
226
+ const warn = options.warn ?? warning
227
+ let raw
228
+ try {
229
+ raw = probe()
230
+ } catch (err) {
231
+ warn(
232
+ `Could not read the host OpenCode version (${err.message}). Falling back to the V1 ` +
233
+ `configuration; if this host is OpenCode 2.x re-run with --opencode-version v2 ` +
234
+ `(or v1 to force V1).`,
235
+ )
236
+ return 'v1'
237
+ }
238
+
239
+ // Accept both the real probe's {stdout, stderr} shape and a bare string, so a
240
+ // caller (or CI) can inject a probe without spawning a host binary.
241
+ const streams =
242
+ raw !== null && typeof raw === 'object'
243
+ ? { stdout: String(raw.stdout ?? ''), stderr: String(raw.stderr ?? '') }
244
+ : { stdout: String(raw ?? ''), stderr: '' }
245
+
246
+ // stdout decides alone when it yields a version; stderr is a FALLBACK for the
247
+ // host that prints nothing there, never a second vote. If both parse, stdout
248
+ // wins, so a stderr banner can never overrule the primary source.
249
+ const parsed = parseHostMajor(streams.stdout) ?? parseHostMajor(streams.stderr)
250
+
251
+ if (parsed === null) {
252
+ // A probe that RAN but whose output we cannot read is just as dangerous as
253
+ // one that failed: silently landing on V1 is the bug this gate exists to
254
+ // fix, so it gets the same visible warning.
255
+ warn(
256
+ `Could not parse the host OpenCode version from ` +
257
+ `${JSON.stringify(`${streams.stdout}${streams.stderr}`.trim())}. ` +
258
+ `Falling back to the V1 configuration; if this host is OpenCode 2.x re-run with ` +
259
+ `--opencode-version v2 (or v1 to force V1).`,
260
+ )
261
+ return 'v1'
262
+ }
263
+ if (parsed.unreadable !== undefined) {
264
+ // The host named itself and reported a major this gate refuses to read (0,
265
+ // or a calendar year). That is not noise like a trailing build date — it is
266
+ // the host's own version, so letting a surviving runtime token decide would
267
+ // be a silent guess. Say what we saw and fall back loudly.
268
+ warn(
269
+ `The host OpenCode version output reports major ${parsed.unreadable}, which is ` +
270
+ `not a release number this gate can interpret (a prerelease placeholder or a ` +
271
+ `calendar year). Falling back to the V1 configuration; pass --opencode-version v2 ` +
272
+ `if this host is OpenCode 2.x (or v1 to force V1).`,
273
+ )
274
+ return 'v1'
275
+ }
276
+ if (parsed.ambiguous) {
277
+ // Version-shaped tokens that disagree, with no tool name to break the tie.
278
+ // The reviewer accepted "wrong but loud" here; guessing silently is the
279
+ // defect this branch exists to prevent.
280
+ warn(
281
+ `The host OpenCode version output has contradictory versions ` +
282
+ `(${parsed.ambiguous.map((major) => `major ${major}`).join(', ')}) and does not say ` +
283
+ `which is the host's. Falling back to the V1 configuration; pass ` +
284
+ `--opencode-version v2 if this host is OpenCode 2.x (or v1 to force V1).`,
285
+ )
286
+ return 'v1'
287
+ }
288
+ return parsed.major >= 2 ? 'v2' : 'v1'
47
289
  }