@klhapp/skillmux 1.2.0 → 1.3.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.
@@ -0,0 +1,199 @@
1
+ # Troubleshooting
2
+
3
+ Start with:
4
+
5
+ ```sh
6
+ skillmux doctor
7
+ skillmux config show
8
+ skillmux config validate
9
+ ```
10
+
11
+ Add `--json` when you need machine-readable diagnostics.
12
+
13
+ ## Vault failures
14
+
15
+ ### Vault path does not exist
16
+
17
+ Check the effective path and its source:
18
+
19
+ ```sh
20
+ skillmux config show
21
+ ```
22
+
23
+ Create the directory or initialize config against a populated vault:
24
+
25
+ ```sh
26
+ skillmux config init --vault ~/skills --yes
27
+ ```
28
+
29
+ `config init` requires at least one valid `SKILL.md`.
30
+
31
+ ### A skill does not appear
32
+
33
+ Each skill must sit one directory below the vault:
34
+
35
+ ```text
36
+ ~/skills/<skill-id>/SKILL.md
37
+ ```
38
+
39
+ Run:
40
+
41
+ ```sh
42
+ skillmux scan ~/skills/<skill-id>
43
+ skillmux index
44
+ skillmux skill which <skill-id>
45
+ ```
46
+
47
+ The scanner reports invalid frontmatter and unreadable content. `skill which`
48
+ also reveals local-overlay shadowing.
49
+
50
+ ## Sync failures
51
+
52
+ ### Target has no ownership marker
53
+
54
+ Skillmux will not change an existing unmarked directory. Adopt it first:
55
+
56
+ ```sh
57
+ skillmux init --client claude-code --dry-run
58
+ skillmux init --client claude-code --yes
59
+ ```
60
+
61
+ ### Target points to the full vault
62
+
63
+ Review the smaller pinned set before converting:
64
+
65
+ ```sh
66
+ skillmux init --client claude-code \
67
+ --migrate-full-vault \
68
+ --core code-context \
69
+ --dry-run
70
+ ```
71
+
72
+ Apply the same command with `--yes` after checking the plan.
73
+
74
+ ### Unmanaged file collides with a pin
75
+
76
+ Skillmux preserves unmanaged target content. Rename or remove the conflicting
77
+ entry yourself, then rerun `skillmux sync`.
78
+
79
+ ### Target belongs to another host
80
+
81
+ New targets include the current hostname. `sync` skips a target when its
82
+ manifest `host` differs. Run `skillmux target show <name>` and add a separate
83
+ target for the current machine instead of reusing the other machine's path.
84
+
85
+ ### A local-overlay skill cannot be pinned
86
+
87
+ Core and project pins must exist in the canonical `vault_path`. Copy or commit
88
+ the skill there before pinning it. Routed lookup can still serve the overlay.
89
+
90
+ ## Retrieval failures
91
+
92
+ ### `resolve_skill` returns `ambiguous`
93
+
94
+ Ambiguity is the expected result without calibrated reranker thresholds. The
95
+ calling model should select a candidate and call `fetch_skill`.
96
+
97
+ Improve a weak shortlist by:
98
+
99
+ - writing a concrete skill description with task vocabulary;
100
+ - enabling embeddings;
101
+ - increasing recall depth when the relevant skill falls outside the fused
102
+ candidate set.
103
+
104
+ Use a labelled dataset and [Policy calibration](calibration.md) before enabling
105
+ automatic matches.
106
+
107
+ ### Server reports lexical mode
108
+
109
+ Lexical mode means Skillmux can query FTS5 but cannot use embeddings. The next
110
+ step depends on the installation:
111
+
112
+ | Installation | Expected action |
113
+ | --- | --- |
114
+ | Skillmux CLI with local inference | Download the local model and rebuild the index |
115
+ | Full Docker image | Confirm the running tag is the full image and inspect `doctor` output |
116
+ | Slim Docker image | Configure remote embeddings or keep lexical fallback |
117
+
118
+ For a Skillmux CLI installation, run:
119
+
120
+ ```sh
121
+ skillmux doctor
122
+ skillmux models download
123
+ skillmux index
124
+ ```
125
+
126
+ For remote inference, verify the endpoint, model, dimension, API-key
127
+ environment variable, and network path. The slim image does not contain
128
+ GTE-small, so `models download` is not its recovery path.
129
+
130
+ ### Reranker is unavailable
131
+
132
+ Skillmux keeps the hybrid shortlist when a reranker probe or request fails.
133
+ Check `inference.reranker.endpoint`, `adapter`, `model`, and the environment
134
+ variable named by `api_key_env`.
135
+
136
+ Use the complete request URL. Skillmux does not append `/rerank` or infer an
137
+ adapter from the endpoint.
138
+
139
+ ### Delivered content changed after indexing
140
+
141
+ Skillmux checks the file before delivery and refreshes stale metadata. If a
142
+ skill cannot be parsed during a live edit, the index keeps the previous good
143
+ metadata but does not serve stale body bytes. Finish the write with valid
144
+ frontmatter and retry.
145
+
146
+ ## HTTP failures
147
+
148
+ ### Another machine cannot connect
149
+
150
+ The Skillmux CLI binds HTTP to `127.0.0.1`. Set `server.hostname` or
151
+ `HTTP_HOSTNAME` to a reachable interface, then restart the server. Docker
152
+ binds `0.0.0.0` inside the container, but the host still needs a published
153
+ port. Enable authentication before exposing either deployment.
154
+
155
+ ### Browser receives `403`
156
+
157
+ Add the browser origin to `server.allowed_origins`. The value must match the
158
+ request's `Origin` header. Curl and server-to-server clients omit this header
159
+ and do not use the CORS list.
160
+
161
+ ### Client receives `401`
162
+
163
+ Confirm `server.auth_enabled = true`, export the environment variable named by
164
+ `auth_token_env`, and send `Authorization: Bearer <token>`.
165
+
166
+ An enabled server with an empty token environment variable returns a server
167
+ configuration error rather than accepting an empty token.
168
+
169
+ ### Client receives `429`
170
+
171
+ The rate limiter rejected the request. Read `Retry-After` and the
172
+ `X-RateLimit-*` response headers. Increase `requests_per_minute` only after
173
+ checking for a retry loop or shared-token traffic.
174
+
175
+ ## Configuration migration errors
176
+
177
+ Skillmux rejects removed fields with migration guidance:
178
+
179
+ - replace `[targets.<name>].project` with `project_groups = [...]`;
180
+ - rename `[project.<group>].repos` to `paths`;
181
+ - use `skillmux core pin|unpin` instead of removed `manifest pin|unpin`;
182
+ - replace legacy reranker base-URL variables with
183
+ `SKILLMUX_RERANK_ENDPOINT` and `SKILLMUX_RERANK_ADAPTER`.
184
+
185
+ Run `skillmux config validate` after editing TOML.
186
+
187
+ ## Collect diagnostics
188
+
189
+ For a bug report, include:
190
+
191
+ ```sh
192
+ skillmux --help
193
+ skillmux config show
194
+ skillmux doctor --json
195
+ ```
196
+
197
+ Remove tokens, private endpoint credentials, local usernames, and raw audit
198
+ queries before posting output. Open an issue at
199
+ <https://github.com/klhq/skillmux/issues>.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@klhapp/skillmux",
3
- "version": "1.2.0",
4
- "description": "Local read-only MCP server routing natural-language task queries to skills in a SKILL.md vault, with zero-loss delivery",
3
+ "version": "1.3.1",
4
+ "description": "Skill management and retrieval for AI agents: sync native skills across clients and route the long tail over MCP",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Lance Hsu <lance@klh.app>",
@@ -22,10 +22,7 @@
22
22
  },
23
23
  "files": [
24
24
  "src",
25
- "docs/schema.json",
26
- "docs/configuration.md",
27
- "docs/calibration.md",
28
- "docs/releasing.md",
25
+ "docs",
29
26
  "README.md",
30
27
  "LICENSE",
31
28
  "CHANGELOG.md",
package/src/cli.ts CHANGED
@@ -132,6 +132,31 @@ const KNOWN_COMMANDS = [
132
132
  "local-vault",
133
133
  ];
134
134
 
135
+ const DOCKER_HOST_MANAGEMENT_GUIDANCE =
136
+ "This command manages local Skillmux or agent directories and is not supported inside the Docker image. Install the Skillmux CLI on the host using the Bun package or standalone Linux executable.";
137
+
138
+ function isDockerHostManagementCommand(command: string, subCommand: string): boolean {
139
+ if (
140
+ [
141
+ "init",
142
+ "sync",
143
+ "install",
144
+ "project",
145
+ "target",
146
+ "core",
147
+ "local-vault",
148
+ "models",
149
+ "context",
150
+ "calibrate",
151
+ "eval",
152
+ ].includes(command)
153
+ ) {
154
+ return true;
155
+ }
156
+
157
+ return command === "config" && ["init", "set"].includes(subCommand);
158
+ }
159
+
135
160
  async function main() {
136
161
  const rawArgv = Bun.argv.slice(2);
137
162
 
@@ -141,6 +166,8 @@ async function main() {
141
166
  let flagContext: string | undefined;
142
167
  let flagServer: string | undefined;
143
168
  let isDryRun = false;
169
+ const subCommand = rawArgv[1] ?? "";
170
+ const commandArgs = rawArgv.slice(2);
144
171
 
145
172
  const command = rawArgv[0];
146
173
  if (!command || command === "--help" || command === "-h") {
@@ -161,6 +188,18 @@ async function main() {
161
188
 
162
189
  let resolvedTarget: ResolvedTarget = { type: "local", name: "local" };
163
190
 
191
+ if (
192
+ process.env.RUNNING_IN_DOCKER === "true" &&
193
+ isDockerHostManagementCommand(command, subCommand)
194
+ ) {
195
+ handleError(new Error(DOCKER_HOST_MANAGEMENT_GUIDANCE), {
196
+ target: resolvedTarget,
197
+ isJson,
198
+ isVerbose,
199
+ });
200
+ return;
201
+ }
202
+
164
203
  // Only resolve target if command is target-aware or context/config/calibrate
165
204
  const isLocalConfigInit = command === "config" && rawArgv[1] === "init";
166
205
  if (
@@ -181,8 +220,6 @@ async function main() {
181
220
  }
182
221
 
183
222
  const adapter = createTargetAdapter(resolvedTarget, { allowInsecure });
184
- const subCommand = rawArgv[1] ?? "";
185
- const commandArgs = rawArgv.slice(2);
186
223
 
187
224
  try {
188
225
  switch (command) {
@@ -409,7 +446,7 @@ async function handleCalibrateCommand(
409
446
  if (!Number.isFinite(value)) throw new Error(`${flag} must be a number`);
410
447
  return value;
411
448
  };
412
- for (let i = 0; i < args.length; i++) {
449
+ for (let i = 1; i < args.length; i++) {
413
450
  const option = args[i];
414
451
  if (option === "--dataset") {
415
452
  datasetPath = args[++i];
@@ -1,4 +1,4 @@
1
- import { watch } from "node:fs";
1
+ import { mkdirSync, watch } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { loadConfig } from "./config";
4
4
  import type { Config } from "./types";
@@ -133,6 +133,10 @@ export class ConfigWatcher {
133
133
  const dir = dirname(tomlPath);
134
134
  const filename = tomlPath.split(/[/\\]/).pop()!;
135
135
 
136
+ // A config file is optional. Ensure its parent exists so zero-config
137
+ // startup is safe and later config writes are still observed.
138
+ mkdirSync(dir, { recursive: true });
139
+
136
140
  this.watcher = watch(dir, { recursive: false }, (_event, changedName) => {
137
141
  if (this.stopped) return;
138
142
  // Fire for: the config file itself, or any .tmp variant of it (handles