@klhapp/skillmux 0.5.0 → 1.0.0
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/CHANGELOG.md +67 -0
- package/README.md +44 -8
- package/docs/configuration.md +13 -10
- package/package.json +1 -1
- package/src/adapters.ts +11 -5
- package/src/cli.ts +1286 -328
- package/src/completions.ts +95 -36
- package/src/config-watcher.ts +63 -2
- package/src/doctor.ts +19 -1
- package/src/init-clients.ts +31 -0
- package/src/manifest.ts +101 -2
- package/src/output.ts +26 -29
- package/src/project-setup.ts +36 -0
- package/src/prompts.ts +69 -0
- package/src/server.ts +228 -63
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,73 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.0](https://github.com/klhq/skillmux/compare/v0.6.0...v1.0.0) (2026-07-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* **cli:** init's and target add's custom-directory flag is now --dir instead of --path (matches the manifest schema's own "dir" field on [targets.*] entries) - old --path is removed, no alias.
|
|
14
|
+
* **cli:** bare "skillmux which <skill_id>" is removed. Every other command follows a noun-verb shape (target list, project pin); "which" was the one bare-verb holdout. It's now "skillmux skill which <skill_id>", giving it a noun to sit under (room for future skill-level introspection subcommands). Running the bare form fails with a specific error naming the replacement, echoing back the argument the user typed so the suggested command is directly copy-pasteable.
|
|
15
|
+
* **cli:** skillmux manifest pin/unpin no longer exists. [core] pinning is now skillmux core pin/unpin (added in the prior commit); [project.*] pinning was already fully covered by skillmux project pin/unpin. Running skillmux manifest now fails with a specific error naming both replacements instead of falling through to a generic "unknown command" message.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* **cli:** add --json to scan, report, install, eval, models download ([456ab33](https://github.com/klhq/skillmux/commit/456ab330ed8b11d08a74bddf08a1827da4b18c19))
|
|
20
|
+
* **cli:** add core pin/unpin command ([c8b2387](https://github.com/klhq/skillmux/commit/c8b2387e01ac99a9e349cdf4ef2b57bb6f241820))
|
|
21
|
+
* **cli:** bring doctor and local-vault init onto the shared options contract ([2517571](https://github.com/klhq/skillmux/commit/25175719ae175bc8aa22d667a27a462b6286b33b))
|
|
22
|
+
* **cli:** remove manifest command in favor of core/project ([1a38b22](https://github.com/klhq/skillmux/commit/1a38b22e51c54e764c11ad5c8400a03537f01929))
|
|
23
|
+
* **cli:** rename --path to --dir on init/target add; disambiguate target vs context ([0aa704b](https://github.com/klhq/skillmux/commit/0aa704b584c377dbb4c67d87690aae6af9a78320))
|
|
24
|
+
* **cli:** rename which to skill which for noun-verb consistency ([dc5b75e](https://github.com/klhq/skillmux/commit/dc5b75ea3dddf9623e7b1a11bc3650fcf4804162))
|
|
25
|
+
* **completions:** generate bash/zsh/fish from one shared command list ([1431c36](https://github.com/klhq/skillmux/commit/1431c365a3e10071405f4c7d3e072da435018cf7))
|
|
26
|
+
* **doctor:** validate the manifest as part of skillmux doctor ([#67](https://github.com/klhq/skillmux/issues/67)) ([8749245](https://github.com/klhq/skillmux/commit/87492450b9af595c10e0f77c21f2371189b5b09f))
|
|
27
|
+
* **manifest:** bulk pin/unpin for manifest --core ([#66](https://github.com/klhq/skillmux/issues/66)) ([0ded6ea](https://github.com/klhq/skillmux/commit/0ded6ea7e58dfccd236582d433a32606f8ec9d36))
|
|
28
|
+
* **output:** add emitSuccess helper for consistent JSON-envelope output ([7e06551](https://github.com/klhq/skillmux/commit/7e06551a154fdb41452d047f470a89e74e81c570))
|
|
29
|
+
* **output:** add typed CliError for exit-code classification ([a88e4f9](https://github.com/klhq/skillmux/commit/a88e4f9e67b64717ab28a82bc829a84a615459d8))
|
|
30
|
+
* **server:** activate config watcher runtime ([#74](https://github.com/klhq/skillmux/issues/74)) ([ba575ae](https://github.com/klhq/skillmux/commit/ba575ae855514a0a816c39dd95e00a763fd7db9d))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
* **cli:** core pin --dry-run --json and which-removal message edge cases ([082ca53](https://github.com/klhq/skillmux/commit/082ca53a811df680287b142da97de702a8a1d764))
|
|
36
|
+
* **config:** enforce live reload policy ([#73](https://github.com/klhq/skillmux/issues/73)) ([4382356](https://github.com/klhq/skillmux/commit/43823564ce24c19460be5f7e9bd4bb3535594d65))
|
|
37
|
+
* **output:** stop misclassifying exit codes by message-substring matching ([acd958f](https://github.com/klhq/skillmux/commit/acd958f127a307c3af1bc20964fce5d906f92485))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
* **cli:** consolidate command plumbing ([#72](https://github.com/klhq/skillmux/issues/72)) ([9fb3e5d](https://github.com/klhq/skillmux/commit/9fb3e5de1aa45790fbb3e1fb3f760e82fd28b027))
|
|
43
|
+
* **cli:** use emitSuccess instead of hand-rolled envelope printing ([ea4c3a5](https://github.com/klhq/skillmux/commit/ea4c3a5ebcf85fa041b7a279ae8ee27d9da8a1fa))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Docs
|
|
47
|
+
|
|
48
|
+
* rewrite manifest pin, which, and --path references for the CLI consolidation ([0a81070](https://github.com/klhq/skillmux/commit/0a810705ba04e4bfef96a8098fb0b6c8def86230))
|
|
49
|
+
|
|
50
|
+
## [0.6.0](https://github.com/klhq/skillmux/compare/v0.5.0...v0.6.0) (2026-07-24)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
* **cli:** add line-oriented selection prompts ([b8579a4](https://github.com/klhq/skillmux/commit/b8579a47513a722c76d8826b80ca08155f2229f6))
|
|
56
|
+
* **init:** add guided client and project setup ([c426502](https://github.com/klhq/skillmux/commit/c426502693755185a7d83f3c4b8c2b6b502bbb90))
|
|
57
|
+
* **init:** detect installed clients for setup ([0c529f3](https://github.com/klhq/skillmux/commit/0c529f310de8bc96d18ddb6b25174d375478d9fa))
|
|
58
|
+
* **init:** launch guided client setup in terminals ([9bd15ac](https://github.com/klhq/skillmux/commit/9bd15acfbad088732ad6d92927f0e16a2f57f44d))
|
|
59
|
+
* **project:** add direct project management commands ([132e8af](https://github.com/klhq/skillmux/commit/132e8af0124ee75163161df16af83dc774b773a8))
|
|
60
|
+
* **project:** add direct project management commands ([f36e2b3](https://github.com/klhq/skillmux/commit/f36e2b356f5f170c887734367cebb838acacc2e4))
|
|
61
|
+
* **project:** add project init command ([3cf8ecc](https://github.com/klhq/skillmux/commit/3cf8ecc94fb16f0f8ed807976e36935e09918a4e))
|
|
62
|
+
* **project:** harden project setup planning ([faff0e1](https://github.com/klhq/skillmux/commit/faff0e14a6d0a6268da3e01a33553efd9ede7d58))
|
|
63
|
+
* **project:** upsert project setup state ([7a92e03](https://github.com/klhq/skillmux/commit/7a92e03e281b517dff45d11eef5714d0512fd952))
|
|
64
|
+
* **setup:** guide machine and project choices ([2ef1010](https://github.com/klhq/skillmux/commit/2ef1010e8cb0d42b20a6f60980e7be0e28bf1e5f))
|
|
65
|
+
* **target:** add advanced target management commands ([bde2069](https://github.com/klhq/skillmux/commit/bde206981bcb93c0b1be317ef7f1c8d6f6ece34c))
|
|
66
|
+
* **target:** add advanced target management commands ([9e6a993](https://github.com/klhq/skillmux/commit/9e6a993643865b2134be05ba7306c671023f38ff))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
* **cli:** keep guided init interactive and legacy-safe ([f6ae405](https://github.com/klhq/skillmux/commit/f6ae40564ed26b19ac464847b455d840d78daa93))
|
|
72
|
+
* **project:** validate guided project inputs ([0bed954](https://github.com/klhq/skillmux/commit/0bed9543897e83860038d83620a7c3638540ca95))
|
|
73
|
+
* **setup:** report client target prerequisites ([62fd8b8](https://github.com/klhq/skillmux/commit/62fd8b80444a37c8bcde403be95d8224b9776b0b))
|
|
74
|
+
|
|
8
75
|
## [0.5.0](https://github.com/klhq/skillmux/compare/v0.4.5...v0.5.0) (2026-07-24)
|
|
9
76
|
|
|
10
77
|
|
package/README.md
CHANGED
|
@@ -186,6 +186,11 @@ bun run src/cli.ts serve
|
|
|
186
186
|
|
|
187
187
|
Optional — skip this if `resolve_skill` alone is enough (most setups). Use it once you want a small set of skills loaded *statically* in every agent that reads from a given directory, instead of routed on demand — see [Tiers](#tiers-routed-vs-pinned).
|
|
188
188
|
|
|
189
|
+
Run `skillmux init` with no arguments in a terminal for guided setup. It checks
|
|
190
|
+
the vault, preselects clients with concrete filesystem evidence, asks for core
|
|
191
|
+
skills, shows one review, and applies after one confirmation. The wizard uses
|
|
192
|
+
line-oriented prompts and does not take over the terminal screen.
|
|
193
|
+
|
|
189
194
|
### 1. Check the plan
|
|
190
195
|
|
|
191
196
|
```sh
|
|
@@ -213,10 +218,13 @@ skillmux init --client claude-code --client codex --core csv-formatter --yes
|
|
|
213
218
|
```
|
|
214
219
|
|
|
215
220
|
Skillmux writes `skillmux.toml`, adds host-scoped targets, adopts each selected
|
|
216
|
-
directory, and installs one managed discovery block
|
|
217
|
-
instruction file.
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
directory, and installs one managed discovery block for each client with a
|
|
222
|
+
known safe durable instruction file. Other clients report manual setup.
|
|
223
|
+
Existing manifest entries and instruction text stay intact.
|
|
224
|
+
Explicit flags keep the command deterministic. Without `--yes`, an interactive
|
|
225
|
+
terminal reviews the plan before applying; a pipe or CI job must pass `--yes`.
|
|
226
|
+
Use `--no-instructions` to leave instruction files untouched or `--no-sync` to
|
|
227
|
+
save setup for a later `skillmux sync`.
|
|
220
228
|
|
|
221
229
|
Use direct targets when you want paths rather than clients:
|
|
222
230
|
|
|
@@ -224,7 +232,7 @@ Use direct targets when you want paths rather than clients:
|
|
|
224
232
|
skillmux init --target agent-skills --yes
|
|
225
233
|
skillmux init --target claude-code --yes
|
|
226
234
|
skillmux init --target codex --yes
|
|
227
|
-
skillmux init --target custom --
|
|
235
|
+
skillmux init --target custom --dir /srv/my-agent/skills --yes
|
|
228
236
|
```
|
|
229
237
|
|
|
230
238
|
The old `agents` and `claude` target names still work and print a deprecation
|
|
@@ -244,16 +252,44 @@ skillmux init --client claude-code --migrate-full-vault \
|
|
|
244
252
|
After conversion and `skillmux sync`, the client sees the selected core pins
|
|
245
253
|
instead of every vault skill.
|
|
246
254
|
|
|
247
|
-
### 3.
|
|
255
|
+
### 3. Set up project-scoped skills
|
|
256
|
+
|
|
257
|
+
From a project directory:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
skillmux project init
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The guided flow detects the Git root, suggests a project-group name, asks which
|
|
264
|
+
configured clients should receive the group, and accepts project skill IDs.
|
|
265
|
+
For automation:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
skillmux project init ~/code/skillmux --name skillmux \
|
|
269
|
+
--client claude-code --client codex \
|
|
270
|
+
--skill sdd-tdd --skill code-context --yes
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Re-running the command adds missing paths, skills, and target attachments
|
|
274
|
+
without duplicating existing entries. Project setup runs `sync` unless
|
|
275
|
+
`--no-sync` is supplied.
|
|
276
|
+
|
|
277
|
+
### 4. Add or remove pins later
|
|
248
278
|
|
|
249
279
|
`--core` seeds only the skill IDs you name. Without it, `init` preserves
|
|
250
280
|
existing core pins and makes no guesses. Add another pin with:
|
|
251
281
|
|
|
252
282
|
```sh
|
|
253
|
-
skillmux
|
|
283
|
+
skillmux core pin csv-formatter --yes
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Pin several skills to `[core]` in one atomic call by passing more than one `skill_id`:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
skillmux core pin csv-formatter pdf-extractor terraform-plans --yes
|
|
254
290
|
```
|
|
255
291
|
|
|
256
|
-
Unpin the same way: `skillmux
|
|
292
|
+
Unpin the same way: `skillmux core unpin csv-formatter --yes` (or `skillmux core unpin csv-formatter pdf-extractor --yes` for several at once). `skillmux project pin <group> <skill_id>...` pins into an existing `[project.<group>]` tier instead — create the group first with `skillmux project add-path <group> <path> --yes` if it doesn't exist yet. Every pin is validated before writing — the skill must actually resolve from the vault, and `[core]` stays under its 25-skill cap. When multiple `skill_id`s are given, they're applied to a single in-memory manifest and written once: if any one of them is already pinned elsewhere (or already unpinned, for `unpin`), the whole call fails and nothing is written.
|
|
257
293
|
|
|
258
294
|
Hand-editing `skillmux.toml` still works if you prefer it:
|
|
259
295
|
|
package/docs/configuration.md
CHANGED
|
@@ -118,23 +118,26 @@ project_groups = ["repo1"] # which [project.*] groups materialize into
|
|
|
118
118
|
- `[project.<group>].paths` can list the same project's checkout on more than one machine (e.g. `["/home/alice/code/repo1", "/Users/alice/code/repo1"]`) — `sync` silently skips any entry that doesn't exist on the machine it's running on (see below), so one shared manifest can span machines with different checkout locations without needing per-machine manifests.
|
|
119
119
|
- `[targets.<name>]` — one entry per adopted surface. `skillmux init --target <name> --yes` writes these and scopes newly added targets to the current hostname. Hand-editing is fine as long as `sync` is still allowed to own the directory (see below). An optional `host` limits the target to an exact machine-hostname match; omit it for a global, backward-compatible target. A host mismatch is reported and skipped before any target filesystem operation. `project_groups` is an explicit list, not a boolean — a target only receives the specific groups it names, never every group in the manifest.
|
|
120
120
|
|
|
121
|
-
**Pin/unpin without hand-editing.** `skillmux
|
|
121
|
+
**Pin/unpin without hand-editing.** `skillmux core pin`/`unpin` mutate `[core]` for you, and `skillmux project pin`/`unpin` mutate `[project.*]`, validating with the same rules `sync` enforces (skill must resolve from `vault_path`, no duplicate pins, `[core]` stays under the 25-skill cap) before writing anything:
|
|
122
122
|
|
|
123
123
|
```sh
|
|
124
|
-
skillmux
|
|
125
|
-
skillmux
|
|
126
|
-
skillmux
|
|
127
|
-
skillmux
|
|
128
|
-
skillmux
|
|
124
|
+
skillmux core pin csv-formatter --yes # add to [core]
|
|
125
|
+
skillmux core pin csv-formatter pdf-extractor terraform-plans --yes # pin several skills to [core] in one atomic call
|
|
126
|
+
skillmux project pin repo1 pdf-extractor --yes # add to an existing group
|
|
127
|
+
skillmux core unpin csv-formatter --yes # remove from [core]
|
|
128
|
+
skillmux core unpin csv-formatter pdf-extractor --yes # unpin several skills from [core] in one atomic call
|
|
129
|
+
skillmux project unpin repo1 pdf-extractor --yes # remove from a group (group stays, even if empty)
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
Both commands accept one or more `skill_id` arguments per call; all of them are validated and applied against a single in-memory manifest before anything is written, so if any one of them is already pinned elsewhere (or, for unpin, not currently pinned), the whole call fails and the manifest file is left untouched — no partial pins. To pin into a `[project.<group>]` tier that doesn't exist yet, create it first with `skillmux project add-path <group> <path> --yes`. Hand-editing `skillmux.toml` directly is still fully supported; these commands are a convenience layer over the same file, not a replacement for it.
|
|
132
133
|
|
|
134
|
+
> **Breaking change:** `skillmux manifest pin`/`unpin` is removed. `[core]` pinning is now `skillmux core pin`/`unpin`; `[project.*]` pinning was already available as `skillmux project pin`/`unpin` and is now the only way to do it — there's no more `--path`-based inline group creation from a pin call, use `project add-path` to create the group first.
|
|
135
|
+
>
|
|
133
136
|
> **Breaking change:** `[targets.<name>].project` (a boolean) has been replaced by `project_groups` (an array of `[project.*]` names). A manifest still using the old field fails to parse with an error pointing at the new one. To migrate, replace `project = true` with `project_groups = [...]` listing every group that target previously received (previously *all* groups, unconditionally); replace `project = false` with `project_groups = []`.
|
|
134
137
|
>
|
|
135
138
|
> **Breaking change:** `[project.<group>].repos` has been renamed to `paths` — it was never required to be a git repository, just a local directory, and the old name collided in meaning with `skillmux install <repo>`'s unrelated git-source `repo` concept. A manifest still using `repos` fails to parse with an error pointing at `paths`; migrate by renaming the key (values are unchanged).
|
|
136
139
|
|
|
137
|
-
Every `[core]`/`[project.*]` skill_id must resolve from the canonical `vault_path` — pinning a skill that only exists in a `local_vault_paths` entry (see below) fails `sync
|
|
140
|
+
Every `[core]`/`[project.*]` skill_id must resolve from the canonical `vault_path` — pinning a skill that only exists in a `local_vault_paths` entry (see below) fails `sync` with a distinct error, since the manifest is meant to be portable across machines and a machine-local override wouldn't exist elsewhere. `doctor` validates the manifest as part of its checks, surfacing any violation without writing anything back.
|
|
138
141
|
|
|
139
142
|
### Ownership marker
|
|
140
143
|
|
|
@@ -176,9 +179,9 @@ local_vault_paths = ["~/skills-local"] # optional, default []: override-only,
|
|
|
176
179
|
|
|
177
180
|
**Visibility.** A `skill_id` present in more than one root is silently resolved via the precedence above with no output during normal use — two commands make that resolution visible on demand:
|
|
178
181
|
|
|
179
|
-
- `skillmux which <skill_id>` — prints which root actually serves that skill, and names every root it shadows:
|
|
182
|
+
- `skillmux skill which <skill_id>` — prints which root actually serves that skill, and names every root it shadows:
|
|
180
183
|
```
|
|
181
|
-
$ skillmux which my-skill
|
|
184
|
+
$ skillmux skill which my-skill
|
|
182
185
|
my-skill: serving from /home/user/skills-local
|
|
183
186
|
shadows: /home/user/skills
|
|
184
187
|
```
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { applyCalibrationRun, getCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, type CalibrationResult } from "./calibrate";
|
|
4
4
|
import { createClients } from "./clients";
|
|
5
5
|
import { DEFAULT_CONFIG_PATH, expandHome, loadConfig } from "./config";
|
|
6
|
+
import { CliError } from "./output";
|
|
6
7
|
import {
|
|
7
8
|
computeHash,
|
|
8
9
|
getDottedKey,
|
|
@@ -109,7 +110,7 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
109
110
|
async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
|
|
110
111
|
const caps = await this.getCapabilities();
|
|
111
112
|
if (caps.persistence === "externally_managed") {
|
|
112
|
-
throw new
|
|
113
|
+
throw new CliError("Configuration is externally managed and cannot be modified", 4);
|
|
113
114
|
}
|
|
114
115
|
return setDottedKey(key, rawValStr, {
|
|
115
116
|
configPath: this.configPath,
|
|
@@ -232,9 +233,14 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
232
233
|
} catch {
|
|
233
234
|
// text
|
|
234
235
|
}
|
|
236
|
+
if (res.status === 401 || res.status === 403) {
|
|
237
|
+
const message = typeof data === "object" && data ? data.message || data.error || data : data;
|
|
238
|
+
throw new CliError(`Remote server rejected the request (${res.status}): ${message}`, 3);
|
|
239
|
+
}
|
|
235
240
|
return { status: res.status, headers: res.headers, data };
|
|
236
241
|
} catch (err: any) {
|
|
237
|
-
|
|
242
|
+
if (err instanceof CliError) throw err;
|
|
243
|
+
throw new CliError(`Failed to reach remote server "${this.serverUrl}": ${err.message}`, 3);
|
|
238
244
|
}
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -279,7 +285,7 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
279
285
|
async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
|
|
280
286
|
const caps = await this.getCapabilities();
|
|
281
287
|
if (!caps.config_write || caps.persistence === "externally_managed") {
|
|
282
|
-
throw new
|
|
288
|
+
throw new CliError("Remote server configuration is externally managed or read-only", 4);
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
const { status: showStatus, headers: showHeaders, data: showData } = await this.fetchJson("/admin/v1/config");
|
|
@@ -317,10 +323,10 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
317
323
|
|
|
318
324
|
if (status === 409) {
|
|
319
325
|
if (data?.error === "CONFIG_REVISION_CONFLICT") {
|
|
320
|
-
throw new
|
|
326
|
+
throw new CliError(`Revision conflict: ${data.message || "Remote configuration was modified concurrently"}`, 4);
|
|
321
327
|
}
|
|
322
328
|
if (data?.error === "CONFIG_EXTERNALLY_MANAGED") {
|
|
323
|
-
throw new
|
|
329
|
+
throw new CliError("Configuration is externally managed on the remote server", 4);
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
|