pantheon-opencode 1.6.0-beta.4 → 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.
- package/AGENTS.md +24 -5
- package/CHANGELOG.md +27 -0
- package/README.md +31 -11
- package/README.pt-BR.md +15 -6
- package/bin/pantheon-init.mjs +26 -3
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +6 -6
- package/scripts/{code_mode_server.py → code_mode.py} +2 -2
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/config-migration.mjs +80 -2
- package/scripts/install/health-check.mjs +12 -17
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +250 -119
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/scripts/probe-context-rehydrate.mjs +2 -2
- package/scripts/sync-artifacts.mjs +5 -5
- package/scripts/test-opencode-v2-sandbox.sh +21 -21
- package/skills-lock.json +1 -1
- package/src/agents/apollo.md +7 -2
- package/src/agents/zeus.md +29 -1
- package/src/instructions/memory-protocol.instructions.md +15 -4
- package/src/instructions/zeus-anti-stall.instructions.md +9 -1
- package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +113 -0
- package/src/plugin-v2.ts +149 -53
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/skills/context-compression/SKILL.md +1 -1
- package/opencode.json +0 -58
- /package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +0 -0
package/src/plugin-v2.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - 3 self-sufficient tools via V2 tool.transform (hashline_edit,
|
|
6
6
|
* pantheon_cost, pantheon_model — see ./pantheon/v2-tools.ts; the goal tools
|
|
7
7
|
* need V1 infrastructure and are absent from this surface)
|
|
8
|
-
* -
|
|
8
|
+
* - 5 event subscriptions (session.created, idle, deleted, error, compacted)
|
|
9
9
|
* - Session hooks (prompt, context, compaction)
|
|
10
10
|
* - A tool `execute.before` hook that ENFORCES read-only sessions: the agent
|
|
11
11
|
* arrives on the host's event, a read-only agent (apollo/gaia) registers its
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
type V2ContextLike,
|
|
95
95
|
} from './pantheon/v2-bridge.ts'
|
|
96
96
|
import { createV2ToolDefinitions, type V2ToolResult } from './pantheon/v2-tools.ts'
|
|
97
|
+
import { V2_UNSUPPORTED_FEATURE_SEED } from './pantheon/v2-unsupported.mjs'
|
|
97
98
|
import { type HookPayload, runHook } from './plugins/hook-runner.ts'
|
|
98
99
|
|
|
99
100
|
// ─── Unsupported Features Registry ───────────────────────────────────────
|
|
@@ -103,37 +104,18 @@ import { type HookPayload, runHook } from './plugins/hook-runner.ts'
|
|
|
103
104
|
* the observed host. A listed feature is not necessarily a host-absent API.
|
|
104
105
|
* Additional features are appended during setup when a required API is missing.
|
|
105
106
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// `SkillEditor.source()` — for adding a directory source. This is narrower
|
|
119
|
-
// than (and distinct from) ctx.skill's host availability or callable
|
|
120
|
-
// transform, which a live 2.0.22 host does provide.
|
|
121
|
-
'skill-transform',
|
|
122
|
-
// Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
|
|
123
|
-
// GoalLoopClient and a BackgroundJobBoard, none of which the V2
|
|
124
|
-
// PluginContext exposes, and the V1 bridge resolves to null outside V1.
|
|
125
|
-
// pantheon_goal_create/get/update are therefore absent from the V2 surface
|
|
126
|
-
// rather than registered as non-functional placeholders.
|
|
127
|
-
'goal-tools',
|
|
128
|
-
// Adapter limitation, not a host gap: the caller/target delegation matrix
|
|
129
|
-
// needs a session hierarchy seeded from session metadata, and V2 exposes no
|
|
130
|
-
// seed path for it — SessionHierarchyRegistry.isRoot reports `true` for
|
|
131
|
-
// unknown sessions while unseeded, which would deny every `task()` call. The
|
|
132
|
-
// branch is therefore skipped rather than left to deny indiscriminately.
|
|
133
|
-
// Read-only depth-2 still holds via the blocked-tool list in the guard below,
|
|
134
|
-
// so this marker records an unenforced matrix, not an unenforced depth limit.
|
|
135
|
-
'delegation-matrix',
|
|
136
|
-
]
|
|
107
|
+
/**
|
|
108
|
+
* Live unsupported-feature list: the shared seed plus anything `markUnsupported`
|
|
109
|
+
* appends while this process talks to a real host.
|
|
110
|
+
*
|
|
111
|
+
* The seed lives in `src/pantheon/v2-unsupported.mjs` — a plain-`.mjs` module —
|
|
112
|
+
* so that the V2 installer and `scripts/doctor.mjs` report the SAME strings
|
|
113
|
+
* without either of them having to import TypeScript. This array stays a
|
|
114
|
+
* mutable copy rather than the shared seed itself: `markUnsupported` appends to
|
|
115
|
+
* it, and the seed must stay frozen. `getUnsupportedFeatures()` still returns
|
|
116
|
+
* this exact reference.
|
|
117
|
+
*/
|
|
118
|
+
export const V2_UNSUPPORTED_FEATURES: string[] = [...V2_UNSUPPORTED_FEATURE_SEED]
|
|
137
119
|
|
|
138
120
|
// ─── Constants ───────────────────────────────────────────────────────────
|
|
139
121
|
|
|
@@ -508,6 +490,9 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
|
|
|
508
490
|
case 'session.idle':
|
|
509
491
|
await onSessionIdle(event)
|
|
510
492
|
break
|
|
493
|
+
case 'session.deleted':
|
|
494
|
+
await onSessionDeleted(event)
|
|
495
|
+
break
|
|
511
496
|
case 'session.error':
|
|
512
497
|
await onSessionError(event)
|
|
513
498
|
break
|
|
@@ -522,18 +507,44 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
|
|
|
522
507
|
/**
|
|
523
508
|
* Session-created handler: currently a no-op on the V2-only surface.
|
|
524
509
|
*
|
|
525
|
-
* V1 seeds session state here
|
|
526
|
-
* root-session set
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
510
|
+
* V1 seeds session state here — `sessionHierarchy.register(info)` plus the
|
|
511
|
+
* root-session set — from the full session metadata carried by its own event.
|
|
512
|
+
*
|
|
513
|
+
* THE V2 EVENT CARRIES THE SAME METADATA, so this is an UNWIRED seed, not an
|
|
514
|
+
* impossible one. The V2 SDK types `session.created` as
|
|
515
|
+
* `{ sessionID: string; info: Session }`, and `Session` declares
|
|
516
|
+
* `parentID?: string` (`@opencode-ai/sdk` v2 `gen/types.gen.d.ts`) — a superset
|
|
517
|
+
* of the two fields V1 reads on this exact event. Seeding here would be a copy
|
|
518
|
+
* of V1's line. An earlier revision of this comment claimed the metadata was
|
|
519
|
+
* absent from the V2 event; the SDK types refute that, so do not restore it.
|
|
520
|
+
*
|
|
521
|
+
* WHAT IS NOT MEASURED, in either direction: whether a live 2.0.22 host
|
|
522
|
+
* populates `info.parentID` on this event. The tool canary records
|
|
523
|
+
* `Object.keys(ctx)` from inside the host, not event payloads, so host
|
|
524
|
+
* population is UNVERIFIED. What is proven is only that the type carries the
|
|
525
|
+
* field and that no code path here reads it.
|
|
526
|
+
*
|
|
527
|
+
* The real asymmetry is the OTHER of V1's two seed sources. Besides this event,
|
|
528
|
+
* V1 runs a fail-open startup seed from `client.session.list()`
|
|
529
|
+
* (`seedRootSessionsInBackground`, `src/plugin.ts`) to cover sessions that
|
|
530
|
+
* predate plugin load, because opencode does not replay `session.created` for
|
|
531
|
+
* them. V2's `PluginContext` exposes no `client` at all — `options`, `agent`,
|
|
532
|
+
* `aisdk`, `catalog`, `command`, `integration`, `plugin`, `reference`, `skill`,
|
|
533
|
+
* and nothing else — so that enumeration has NO V2 EQUIVALENT. Wiring this
|
|
534
|
+
* handler would recover live sessions only, never resumed ones.
|
|
532
535
|
*
|
|
533
|
-
*
|
|
534
|
-
* `
|
|
535
|
-
*
|
|
536
|
-
*
|
|
536
|
+
* And — more to the point — V1 is a separate plugin instance that is not loaded
|
|
537
|
+
* when only `plugin-v2` is configured, so on a V2-only install nothing seeds
|
|
538
|
+
* the hierarchy at all. Do not read the bridge check below as coverage: a
|
|
539
|
+
* non-null bridge means a host process that ALSO loaded V1, and V1 does the
|
|
540
|
+
* seeding itself.
|
|
541
|
+
*
|
|
542
|
+
* The unwired hierarchy seed is ONE of the two reasons the V2 enforcement guard
|
|
543
|
+
* is built without `isRootSession` / `isChildSession`; the other — and the one
|
|
544
|
+
* that would actually deny every `task()` call — is the absent
|
|
545
|
+
* `getSessionAgent`. See the `v2EnforcementGuard` comment below for both, and
|
|
546
|
+
* `docs/UPGRADING.md`, "Known limitation — V2 enforcement covers the
|
|
547
|
+
* blocked-tool list, not the V1 delegation matrix".
|
|
537
548
|
*/
|
|
538
549
|
async function onSessionCreated(_event: V2SessionEvent): Promise<void> {
|
|
539
550
|
const _bridge = resolveBridge()
|
|
@@ -560,6 +571,58 @@ async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
|
|
|
560
571
|
}
|
|
561
572
|
}
|
|
562
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Session-deleted handler: drop the read-only registration.
|
|
576
|
+
*
|
|
577
|
+
* `readOnlyRegistry` is a process-wide singleton and the `execute.before` hook
|
|
578
|
+
* populates it on every delegated `apollo`/`gaia` call. V1 prunes it on
|
|
579
|
+
* `session.deleted` (`src/plugin.ts`); V2 did not, so on a long-lived host
|
|
580
|
+
* every finished investigation session stayed registered for the life of the
|
|
581
|
+
* process — a slow leak, and a stale entry that would keep denying tools if
|
|
582
|
+
* the id were ever reused.
|
|
583
|
+
*
|
|
584
|
+
* This mirrors V1: unregister the session, which drops its agent entry with it
|
|
585
|
+
* (the registry stores one `ReadOnlyEntry` per session id). V1 also clears a
|
|
586
|
+
* `sessionAgents` map here; V2 has no such map and needs no equivalent, since
|
|
587
|
+
* it reads the active agent off the event instead of caching it.
|
|
588
|
+
*
|
|
589
|
+
* The id is read from `properties.sessionID` — the field the sibling
|
|
590
|
+
* `session.idle` handler already uses — falling back to V1's
|
|
591
|
+
* `properties.info.id`. Both spellings are accepted because the V2 SDK types
|
|
592
|
+
* `session.deleted` as `{ sessionID, info }` and a miss here is a silent leak,
|
|
593
|
+
* which is precisely the bug this handler exists to close.
|
|
594
|
+
*
|
|
595
|
+
* A payload with NEITHER spelling is fail-open, and unavoidably so: unregistering
|
|
596
|
+
* is what needs the id, so an id that cannot be resolved cannot be cleaned up
|
|
597
|
+
* and the entry survives. The docstring used to leave that as a documented
|
|
598
|
+
* silent branch; it now warns on the way out, because the only other symptom of
|
|
599
|
+
* a renamed field is a registry that grows for the life of the process.
|
|
600
|
+
*/
|
|
601
|
+
async function onSessionDeleted(event: V2SessionEvent): Promise<void> {
|
|
602
|
+
const properties = event.properties
|
|
603
|
+
if (properties === undefined) return
|
|
604
|
+
const info = properties.info
|
|
605
|
+
const sessionID =
|
|
606
|
+
typeof properties.sessionID === 'string'
|
|
607
|
+
? properties.sessionID
|
|
608
|
+
: info !== null && typeof info === 'object'
|
|
609
|
+
? (info as { id?: unknown }).id
|
|
610
|
+
: undefined
|
|
611
|
+
if (typeof sessionID !== 'string' || sessionID === '') {
|
|
612
|
+
// Fail-open by necessity, loud by choice: the entry for this session (if
|
|
613
|
+
// any) cannot be identified, so it stays registered. Same `console.warn`
|
|
614
|
+
// channel as the event-subscription error above — this module has no
|
|
615
|
+
// logger, and a diagnostic nobody reads on stderr is still a silent leak.
|
|
616
|
+
console.warn(
|
|
617
|
+
'[Pantheon V2] session.deleted carried no session id ' +
|
|
618
|
+
'(neither properties.sessionID nor properties.info.id); ' +
|
|
619
|
+
'its read-only registration cannot be pruned and will persist.',
|
|
620
|
+
)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
readOnlyRegistry.unregister(sessionID)
|
|
624
|
+
}
|
|
625
|
+
|
|
563
626
|
/**
|
|
564
627
|
* Session-error handler: no V2-side implementation.
|
|
565
628
|
*
|
|
@@ -768,15 +831,41 @@ interface V2ToolExecuteBeforeEvent {
|
|
|
768
831
|
* tool — `hashline_edit` and `pantheon_model` are denied here for exactly the
|
|
769
832
|
* reason they are denied in V1.
|
|
770
833
|
*
|
|
771
|
-
* Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
834
|
+
* Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession`.
|
|
835
|
+
* There are TWO distinct reasons, and conflating them is what produced a wrong
|
|
836
|
+
* causal chain here once already — keep them apart:
|
|
837
|
+
*
|
|
838
|
+
* 1. MISSING `getSessionAgent` (wiring). This is what would deny every
|
|
839
|
+
* `task()` call. V2 learns the active agent from the `execute.before` event
|
|
840
|
+
* itself and keeps no session→agent map, so there is no lookup to hand the
|
|
841
|
+
* guard. The guard would call `isDelegationAllowed(undefined, target)`,
|
|
842
|
+
* which returns `false`, and throw "caller agent is unavailable" — for
|
|
843
|
+
* every caller, including Zeus. Note what is NOT happening: an unseeded
|
|
844
|
+
* `isRootSession` would *pass* the root gate (`isRoot` reports `true` for an
|
|
845
|
+
* unknown session), so the hierarchy is not what denies here.
|
|
846
|
+
*
|
|
847
|
+
* 2. UNWIRED HIERARCHY (design). `SessionHierarchyRegistry` is what gates
|
|
848
|
+
* exactly two things, and nothing else: the depth-2 child deny
|
|
849
|
+
* (`isChildSession`) and the root-session gate (`isRootSession`). Left
|
|
850
|
+
* unseeded, `isChild` is `false` for every session, so depth-2 would never
|
|
851
|
+
* fire on a genuine child — a hierarchy that cannot tell a child from a root
|
|
852
|
+
* is not a trustworthy input to either check.
|
|
853
|
+
*
|
|
854
|
+
* "Unwired", not "impossible": the V2 `session.created` event carries
|
|
855
|
+
* `properties.info: Session` including `parentID?`, the same field V1 seeds
|
|
856
|
+
* from on the same event, so one line in `onSessionCreated` would seed live
|
|
857
|
+
* sessions (see its docstring). What has no V2 equivalent is V1's SECOND
|
|
858
|
+
* source, the `client.session.list()` startup seed — V2's `PluginContext`
|
|
859
|
+
* exposes no `client`. Whether a live 2.0.22 host populates `info.parentID`
|
|
860
|
+
* is UNVERIFIED; no canary reads an event payload. So the correct summary is
|
|
861
|
+
* "seeded for nothing", not "unseedable", and the guard's omission is right
|
|
862
|
+
* today for the weaker reason that nothing seeds it at all.
|
|
863
|
+
*
|
|
864
|
+
* Omitting all three skips the branch wholesale: the guard only enters it when
|
|
865
|
+
* `options?.isRootSession !== undefined`. `task` is still denied inside a
|
|
866
|
+
* read-only session via the blocked-tool list, so depth-2 holds for
|
|
867
|
+
* apollo/gaia; the caller/target matrix itself is NOT enforced on V2. That is
|
|
868
|
+
* a known gap, not a covered case.
|
|
780
869
|
*/
|
|
781
870
|
const v2EnforcementGuard = createEnforcementGuard({
|
|
782
871
|
getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
|
|
@@ -1115,7 +1204,14 @@ export function v2Dispose(): void {
|
|
|
1115
1204
|
}
|
|
1116
1205
|
|
|
1117
1206
|
/**
|
|
1118
|
-
*
|
|
1207
|
+
* The current list of unsupported V2 features.
|
|
1208
|
+
*
|
|
1209
|
+
* Read by the installer and by `doctor` (both via the shared seed in
|
|
1210
|
+
* `src/pantheon/v2-unsupported.mjs`) so the markers a user is shown are the
|
|
1211
|
+
* same ones this returns, rather than a hand-copied duplicate.
|
|
1212
|
+
*
|
|
1213
|
+
* This returns the LIVE array: the shared seed plus whatever `markUnsupported`
|
|
1214
|
+
* appended while this process talked to a real host.
|
|
1119
1215
|
*/
|
|
1120
1216
|
export function getUnsupportedFeatures(): readonly string[] {
|
|
1121
1217
|
return V2_UNSUPPORTED_FEATURES
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pantheon-tui",
|
|
9
|
-
"version": "1.6.0-beta.
|
|
9
|
+
"version": "1.6.0-beta.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": "1.18.33",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.6.0-beta.
|
|
4
|
+
"version": "1.6.0-beta.5",
|
|
5
5
|
"description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -639,7 +639,7 @@ Allowed fields: file paths, status, pass/fail verdicts, phase names, agent names
|
|
|
639
639
|
|
|
640
640
|
### Layer 2 — Regex pattern scrub (automatic via MCP layer)
|
|
641
641
|
|
|
642
|
-
**Source of truth:** `scripts/scrub-secrets.py` — the single canonical scrubber. Both `
|
|
642
|
+
**Source of truth:** `scripts/scrub-secrets.py` — the single canonical scrubber. Both `memory_mcp.py` and `compress-inline.py` load it via `importlib` (the filename has a hyphen and cannot be `import`ed normally). Do NOT maintain a separate inline pattern list.
|
|
643
643
|
|
|
644
644
|
**Real signature:**
|
|
645
645
|
|
package/opencode.json
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://opencode.ai/config.json",
|
|
3
|
-
"provider": {
|
|
4
|
-
"opencode-go": {
|
|
5
|
-
"models": {
|
|
6
|
-
"mimo-v2.5": {
|
|
7
|
-
"attachment": true,
|
|
8
|
-
"modalities": { "input": ["text", "image"] }
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"default_agent": "zeus",
|
|
14
|
-
"plugins": ["src/plugin-v2"],
|
|
15
|
-
"experimental": {
|
|
16
|
-
"subagent_depth": 2
|
|
17
|
-
},
|
|
18
|
-
"permission": {
|
|
19
|
-
"skill": {
|
|
20
|
-
"*": "allow"
|
|
21
|
-
},
|
|
22
|
-
"websearch": "deny",
|
|
23
|
-
"bash": {
|
|
24
|
-
"git *": "allow",
|
|
25
|
-
"npm *": "allow",
|
|
26
|
-
"npx *": "allow",
|
|
27
|
-
"pytest *": "allow",
|
|
28
|
-
"ruff *": "allow",
|
|
29
|
-
"pip *": "allow",
|
|
30
|
-
"python3 *": "allow"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"mcp": {
|
|
34
|
-
"bifrost": {
|
|
35
|
-
"type": "remote",
|
|
36
|
-
"url": "https://llm.ofertachina.cloud/mcp",
|
|
37
|
-
"enabled": true
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"agent": {
|
|
41
|
-
"talos": {
|
|
42
|
-
"permission": {
|
|
43
|
-
"edit": {
|
|
44
|
-
"*": "allow",
|
|
45
|
-
"**/migrations/**": "deny",
|
|
46
|
-
"**/alembic/**": "deny",
|
|
47
|
-
"**/models/**": "deny",
|
|
48
|
-
"**/auth/**": "deny",
|
|
49
|
-
"**/*.sql": "deny"
|
|
50
|
-
},
|
|
51
|
-
"bash": {
|
|
52
|
-
"*": "allow",
|
|
53
|
-
"alembic *": "deny"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
File without changes
|