@kontextmind/kxm 0.7.165 → 0.7.167
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +16 -0
- package/docs/README.md +1 -0
- package/docs/architecture/hosted-direction.md +9 -4
- package/docs/architecture/inventory.md +1 -0
- package/docs/concepts/trust-model.md +1 -1
- package/docs/contributing/decisions.md +153 -0
- package/docs/contributing/learnings.md +54 -53
- package/docs/contributing/operating-rules.md +19 -6
- package/docs/glossary.md +2 -2
- package/docs/operations/troubleshooting.md +2 -2
- package/docs/operations.md +62 -8
- package/docs/reference/cli-reference.md +11 -11
- package/docs/reference/config-reference.md +36 -11
- package/docs/reference/configuration.md +2 -2
- package/docs/reference/tools.md +1 -1
- package/docs/start/quickstart-claude-code.md +2 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +3 -3
- package/plugins/kxm/dist/claude-hook.js +417 -66
- package/plugins/kxm/dist/cli.js +1399 -900
- package/plugins/kxm/dist/extension.js +399 -64
- package/plugins/kxm/dist/mcp-server.js +622 -135
- package/plugins/kxm/dist/runtime-supervisor.js +668 -166
- package/plugins/kxm/dist/runtime.js +713 -211
- package/plugins/kxm/dist/server.js +466 -58
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +15 -7
- package/plugins/kxm/src/cli/hub.ts +80 -21
- package/plugins/kxm/src/cli/types.ts +8 -1
- package/plugins/kxm/src/cli.ts +41 -7
- package/plugins/kxm/src/config.ts +246 -3
- package/plugins/kxm/src/extension.ts +8 -7
- package/plugins/kxm/src/hub-autostart.ts +20 -0
- package/plugins/kxm/src/hub-env.ts +83 -31
- package/plugins/kxm/src/hub-identity.ts +337 -0
- package/plugins/kxm/src/mcp-server.ts +17 -15
- package/plugins/kxm/src/project-name.ts +6 -47
- package/plugins/kxm/src/server.ts +22 -4
- package/scripts/kxm-hub.mjs +5 -0
package/plugins/kxm/package.json
CHANGED
|
@@ -46,7 +46,7 @@ the hub admin token or a project token.
|
|
|
46
46
|
|---|---|---|
|
|
47
47
|
| `kxm hub start` | Start the hub in the foreground | `--json` |
|
|
48
48
|
| `kxm hub stop` | Request managed hub and worker shutdown | `--wait-ms <ms>` |
|
|
49
|
-
| `kxm hub bind <url>` | Bind this machine to a running hub | http or https URL. `--cloud` with `--token-env
|
|
49
|
+
| `kxm hub bind <url>` | Bind this machine to a running hub and record the non-secret endpoint in `.kxm/config.yaml` | http or https URL. `--token-env` and `--key-op` for either mode. `--cloud` with `--token-env`, `--token-command`, or `--key-op` for an SSH forward to loopback. The token is not stored |
|
|
50
50
|
| `kxm hub unbind` | Remove this machine's hub binding | `--json` |
|
|
51
51
|
| `kxm restore <manifest>` | Replace this project's live stores from a verified backup manifest | `--all-projects`, `--json` |
|
|
52
52
|
|
|
@@ -60,19 +60,27 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
|
|
|
60
60
|
`KXM_AUTH_TOKEN` is unset it generates a long random admin token once and
|
|
61
61
|
persists it in `hub-env.json` (schema `kxm.hub-env.v1`, mode `0600`) under
|
|
62
62
|
the user state root, then reuses it on every restart.
|
|
63
|
-
- `KXM_PROJECT_TOKENS` must list every project's token
|
|
64
|
-
token map rather than merging with it, and the
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
- `KXM_PROJECT_TOKENS` must list every project's token when it is set. It
|
|
64
|
+
replaces the saved token map rather than merging with it, and the
|
|
65
|
+
replacement is saved. The same map can instead live in `.kxm/config.yaml`
|
|
66
|
+
as `hub.projects`, with `op://` references or environment variable names.
|
|
67
|
+
That map is resolved in memory and is not saved. When a hub already serves
|
|
68
|
+
other projects, build the full map with the merge command in the KXM
|
|
69
|
+
documentation's Claude Code quick start
|
|
67
70
|
(docs/start/quickstart-claude-code.md, section "Start the hub") before
|
|
68
71
|
restarting the hub.
|
|
72
|
+
- `kxm hub view` prints the mode, URL, project id, and key source. It does
|
|
73
|
+
not print the token. The project id is the flag, then `KXM_PROJECT`, then
|
|
74
|
+
`hub.local.project` or `hub.cloud.project`, then `.kxm/project.yaml`, then
|
|
75
|
+
the package name, then the directory name.
|
|
69
76
|
- `kxm hub bind` to a remote (non-loopback) URL fails closed with
|
|
70
77
|
`hub_bind_unauthenticated` unless a credential for the current project
|
|
71
78
|
resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`.
|
|
72
79
|
- `kxm hub bind --cloud` is the bind for a loopback SSH forward of a remote
|
|
73
80
|
hub. It never falls back to this machine's hub-env token. Pass
|
|
74
|
-
`--token-env KXMD_HUB_TOKEN
|
|
75
|
-
|
|
81
|
+
`--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
|
|
82
|
+
or `--token-command "op read op://..."`. The non-secret fields are written
|
|
83
|
+
to `.kxm/config.yaml`. See docs/operations.md, section "Cross-box peer attach".
|
|
76
84
|
- Hub PID claims record the wrapper and server child PID. A dead wrapper's
|
|
77
85
|
claim is reclaimed automatically, an orphaned server is terminated first,
|
|
78
86
|
and `kxm hub stop` recovers such orphans directly.
|
|
@@ -3,7 +3,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { redactSecrets } from "../redact.ts";
|
|
5
5
|
import { defaultProjectName } from "../project-name.ts";
|
|
6
|
-
import { agentSessionEnv, hasClientHubCredential, HubEnvError, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
6
|
+
import { agentSessionEnv, hasClientHubCredential, HubEnvError, readHubEnvRecord, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
7
|
+
import { deleteKxmConfigValue, findConfigRoot, isHubOpReference, KxmHubConfigError, writeHubEndpoint } from "../config.ts";
|
|
8
|
+
import { describeHubConnection, opReferenceFromTokenCommand, resolveProjectIdentity } from "../hub-identity.ts";
|
|
7
9
|
import { agentWorker, type Worker } from "../envelope.ts";
|
|
8
10
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
|
|
9
11
|
import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
|
|
@@ -21,7 +23,6 @@ import {
|
|
|
21
23
|
splitTokenCommand,
|
|
22
24
|
validateHubUrl,
|
|
23
25
|
writeHubBinding,
|
|
24
|
-
resolveCloudHubToken,
|
|
25
26
|
type HubBindingRecord,
|
|
26
27
|
type HubHealth,
|
|
27
28
|
} from "../hub-binding.ts";
|
|
@@ -157,11 +158,21 @@ export async function refreshKxmUpdateNotice(
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
export async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
161
|
+
const connection = describeHubConnection(runtime.dirs.workdir, runtime.env, {
|
|
162
|
+
hasSavedProjectToken: (project) => {
|
|
163
|
+
try {
|
|
164
|
+
const token = readHubEnvRecord(runtime.env)?.projectTokens?.[project];
|
|
165
|
+
return typeof token === "string" && token.trim().length > 0;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
160
171
|
let headers: Record<string, string> | undefined;
|
|
161
172
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
headers = { authorization: `Bearer ${
|
|
173
|
+
if (connection.mode === "cloud") {
|
|
174
|
+
const token = resolveClientHubAuthToken(runtime.env, connection.project, runtime.dirs.workdir);
|
|
175
|
+
if (token) headers = { authorization: `Bearer ${token}` };
|
|
165
176
|
}
|
|
166
177
|
} catch (error) {
|
|
167
178
|
if (error instanceof CloudTokenError) {
|
|
@@ -177,19 +188,29 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
|
177
188
|
// Scope is a property of the URL actually contacted, not of whichever file the
|
|
178
189
|
// binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
|
|
179
190
|
// while probing an override would report "loopback" about a remote request.
|
|
180
|
-
// A cloud
|
|
191
|
+
// A cloud mode is remote when the contacted URL is that endpoint, including a
|
|
181
192
|
// loopback SSH forward.
|
|
182
|
-
const effectiveScope =
|
|
183
|
-
const overridden =
|
|
193
|
+
const effectiveScope = connection.scope;
|
|
194
|
+
const overridden = connection.urlSource === "env";
|
|
195
|
+
const identity = {
|
|
196
|
+
mode: connection.mode,
|
|
197
|
+
modeSource: connection.modeSource,
|
|
198
|
+
url: connection.url,
|
|
199
|
+
urlSource: connection.urlSource,
|
|
200
|
+
project: connection.project,
|
|
201
|
+
projectSource: connection.source,
|
|
202
|
+
keySource: connection.keySource,
|
|
203
|
+
};
|
|
184
204
|
const payload = {
|
|
185
205
|
ok: health.ok && ready.ok,
|
|
186
206
|
command: "hub view",
|
|
187
207
|
target: { url: runtime.serverUrl, scope: effectiveScope, ...(overridden ? { source: "env" } : {}) },
|
|
208
|
+
identity,
|
|
188
209
|
health: health.body,
|
|
189
210
|
ready: ready.body,
|
|
190
211
|
};
|
|
191
212
|
print(runtime.io, runtime.json, payload,
|
|
192
|
-
`hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""}`);
|
|
213
|
+
`hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}`);
|
|
193
214
|
return payload.ok ? 0 : 1;
|
|
194
215
|
}
|
|
195
216
|
|
|
@@ -216,10 +237,10 @@ export async function cmdDash(runtime: Runtime, options: { screen?: string | und
|
|
|
216
237
|
runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
|
|
217
238
|
return 2;
|
|
218
239
|
}
|
|
219
|
-
const project =
|
|
240
|
+
const project = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
|
|
220
241
|
let authToken: string | undefined;
|
|
221
242
|
try {
|
|
222
|
-
authToken = resolveClientHubAuthToken(runtime.env, project);
|
|
243
|
+
authToken = resolveClientHubAuthToken(runtime.env, project, runtime.dirs.workdir);
|
|
223
244
|
} catch (error) {
|
|
224
245
|
if (error instanceof CloudTokenError) {
|
|
225
246
|
print(runtime.io, runtime.json, { ok: false, command: "dash", error: error.code, detail: error.message }, error.message);
|
|
@@ -292,6 +313,7 @@ export interface HubBindOptions {
|
|
|
292
313
|
cloud?: boolean | undefined;
|
|
293
314
|
tokenEnv?: string | undefined;
|
|
294
315
|
tokenCommand?: string | undefined;
|
|
316
|
+
keyOp?: string | undefined;
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
|
|
@@ -313,25 +335,35 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
|
|
|
313
335
|
const cloud = options.cloud === true;
|
|
314
336
|
const tokenEnv = options.tokenEnv?.trim() || undefined;
|
|
315
337
|
const tokenCommand = options.tokenCommand?.trim() || undefined;
|
|
316
|
-
|
|
338
|
+
const keyOp = options.keyOp?.trim() || opReferenceFromTokenCommand(tokenCommand ?? "") || undefined;
|
|
339
|
+
if (options.keyOp?.trim() && !isHubOpReference(options.keyOp.trim())) {
|
|
340
|
+
print(
|
|
341
|
+
runtime.io,
|
|
342
|
+
runtime.json,
|
|
343
|
+
{ ok: false, command: "hub bind", error: "hub_config_invalid", url },
|
|
344
|
+
"--key-op must be an op://vault/item/field reference, not a token; no binding was written",
|
|
345
|
+
);
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
if (tokenCommand && !cloud) {
|
|
317
349
|
print(
|
|
318
350
|
runtime.io,
|
|
319
351
|
runtime.json,
|
|
320
352
|
{ ok: false, command: "hub bind", error: "cloud_flag_required", url },
|
|
321
|
-
"--token-
|
|
353
|
+
"--token-command requires --cloud; no binding was written",
|
|
322
354
|
);
|
|
323
355
|
return 2;
|
|
324
356
|
}
|
|
325
|
-
if (cloud && !tokenEnv && !tokenCommand) {
|
|
357
|
+
if (cloud && !tokenEnv && !tokenCommand && !keyOp) {
|
|
326
358
|
print(
|
|
327
359
|
runtime.io,
|
|
328
360
|
runtime.json,
|
|
329
361
|
{ ok: false, command: "hub bind", error: "cloud_token_source_required", url },
|
|
330
|
-
"--cloud needs --token-env, --token-command, or
|
|
362
|
+
"--cloud needs --token-env, --token-command, --key-op, or a combination; the token is not stored",
|
|
331
363
|
);
|
|
332
364
|
return 2;
|
|
333
365
|
}
|
|
334
|
-
if (
|
|
366
|
+
if (tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
|
|
335
367
|
print(
|
|
336
368
|
runtime.io,
|
|
337
369
|
runtime.json,
|
|
@@ -367,10 +399,10 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
|
|
|
367
399
|
if (!cloud && scope === "remote") {
|
|
368
400
|
// The project that will actually authenticate: a record holding only another
|
|
369
401
|
// project's token cannot authorise this one.
|
|
370
|
-
const bindProject =
|
|
402
|
+
const bindProject = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
|
|
371
403
|
let credentialReady = false;
|
|
372
404
|
try {
|
|
373
|
-
credentialReady = hasClientHubCredential(runtime.env, bindProject);
|
|
405
|
+
credentialReady = hasClientHubCredential(runtime.env, bindProject, runtime.dirs.workdir);
|
|
374
406
|
} catch (error) {
|
|
375
407
|
// A malformed record is a readable configuration failure, not an uncaught throw
|
|
376
408
|
// past a user-facing entry point.
|
|
@@ -413,8 +445,9 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
|
|
|
413
445
|
}
|
|
414
446
|
}
|
|
415
447
|
const file = hubBindingFile(runtime.env);
|
|
448
|
+
const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : undefined);
|
|
416
449
|
const cloudFields = cloud
|
|
417
|
-
? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(
|
|
450
|
+
? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(bindingCommand ? { tokenCommand: bindingCommand } : {}) }
|
|
418
451
|
: {};
|
|
419
452
|
const record: HubBindingRecord = {
|
|
420
453
|
schema: HUB_BINDING_SCHEMA,
|
|
@@ -422,19 +455,44 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
|
|
|
422
455
|
boundAt: new Date().toISOString(),
|
|
423
456
|
...cloudFields,
|
|
424
457
|
};
|
|
458
|
+
const identity = resolveProjectIdentity(runtime.dirs.workdir, runtime.env);
|
|
459
|
+
const configFile = join(findConfigRoot(runtime.dirs.workdir), ".kxm", "config.yaml");
|
|
460
|
+
const key = {
|
|
461
|
+
...(keyOp ? { op: keyOp } : {}),
|
|
462
|
+
...(tokenEnv ? { env: tokenEnv } : {}),
|
|
463
|
+
};
|
|
425
464
|
const cloudNote = cloud ? " · token is not stored" : "";
|
|
426
465
|
if (runtime.dryRun) {
|
|
427
466
|
print(
|
|
428
467
|
runtime.io,
|
|
429
468
|
runtime.json,
|
|
430
|
-
{ ok: true, command: "hub bind", dryRun: true, url, scope, file, ...cloudFields },
|
|
469
|
+
{ ok: true, command: "hub bind", dryRun: true, url, scope, file, configFile, project: identity.project, ...cloudFields },
|
|
431
470
|
`would bind hub ${url} (${scope})${cloudNote}`,
|
|
432
471
|
);
|
|
433
472
|
return 0;
|
|
434
473
|
}
|
|
474
|
+
try {
|
|
475
|
+
writeHubEndpoint(runtime.dirs.workdir, {
|
|
476
|
+
mode: cloud ? "cloud" : "local",
|
|
477
|
+
url,
|
|
478
|
+
project: identity.project,
|
|
479
|
+
...(key.op || key.env ? { key } : {}),
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof KxmHubConfigError) {
|
|
483
|
+
print(
|
|
484
|
+
runtime.io,
|
|
485
|
+
runtime.json,
|
|
486
|
+
{ ok: false, command: "hub bind", error: error.code, detail: error.message },
|
|
487
|
+
`${error.message}; no binding was written`,
|
|
488
|
+
);
|
|
489
|
+
return 2;
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
435
493
|
writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
|
|
436
494
|
const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
|
|
437
|
-
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs, ...cloudFields },
|
|
495
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
|
|
438
496
|
`bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
|
|
439
497
|
return 0;
|
|
440
498
|
}
|
|
@@ -463,6 +521,7 @@ export async function cmdHubUnbind(runtime: Runtime): Promise<number> {
|
|
|
463
521
|
return 1;
|
|
464
522
|
}
|
|
465
523
|
removeHubBinding(runtime.env);
|
|
524
|
+
deleteKxmConfigValue(findConfigRoot(runtime.dirs.workdir), "hub.mode");
|
|
466
525
|
if (malformed) {
|
|
467
526
|
print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
|
|
468
527
|
return 0;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
hubBindingFile,
|
|
13
13
|
readHubBinding,
|
|
14
14
|
} from "../hub-binding.ts";
|
|
15
|
+
import { describeHubConnection } from "../hub-identity.ts";
|
|
15
16
|
import type { InstallProbe } from "../kxm-install-kind.ts";
|
|
16
17
|
|
|
17
18
|
export interface CliSpawnResult {
|
|
@@ -166,6 +167,11 @@ export function printWorker(
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
export function hostMode(runtime: Runtime): "local" | "hub" {
|
|
170
|
+
try {
|
|
171
|
+
if (describeHubConnection(runtime.cwd, runtime.env).scope === "remote") return "hub";
|
|
172
|
+
} catch {
|
|
173
|
+
// A malformed binding is classified from the URL below.
|
|
174
|
+
}
|
|
169
175
|
try {
|
|
170
176
|
if (effectiveHubBindingScope(runtime.serverUrl, runtime.env) === "remote") return "hub";
|
|
171
177
|
} catch {
|
|
@@ -282,13 +288,14 @@ export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
|
|
|
282
288
|
throw error;
|
|
283
289
|
}
|
|
284
290
|
}
|
|
291
|
+
const connection = describeHubConnection(ctx.cwd, ctx.env);
|
|
285
292
|
return {
|
|
286
293
|
...ctx,
|
|
287
294
|
json: Boolean(opts.json),
|
|
288
295
|
dryRun: Boolean(opts.dryRun),
|
|
289
296
|
...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
|
|
290
297
|
dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
|
|
291
|
-
serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
298
|
+
serverUrl: connection.url || envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
292
299
|
...(boundHubUrl ? { boundHubUrl } : {}),
|
|
293
300
|
fetchImpl: ctx.io.fetchImpl ?? fetch,
|
|
294
301
|
};
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -17,10 +17,11 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
import { Command, CommanderError } from "commander";
|
|
18
18
|
import { readInstalledKxmVersion } from "./kxm-update.ts";
|
|
19
19
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
20
|
-
import { HubClient } from "./client.ts";
|
|
20
|
+
import { HubClient, HubHttpError } from "./client.ts";
|
|
21
21
|
import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
|
|
22
22
|
import { CloudTokenError } from "./hub-binding.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { KxmHubConfigError } from "./config.ts";
|
|
24
|
+
import { resolveProjectIdentity } from "./hub-identity.ts";
|
|
24
25
|
import {
|
|
25
26
|
AGENT_COMMANDS_MAP,
|
|
26
27
|
enforceToolPolicy,
|
|
@@ -256,13 +257,14 @@ function addGlobalOptions(command: Command): Command {
|
|
|
256
257
|
|
|
257
258
|
async function ensureCliClient(runtime: Runtime): Promise<HubClient> {
|
|
258
259
|
const serverUrl = runtime.serverUrl;
|
|
259
|
-
const
|
|
260
|
+
const identity = resolveProjectIdentity(runtime.cwd, runtime.env);
|
|
261
|
+
const project = identity.project;
|
|
260
262
|
const name = runtime.env.KXM_AGENT_NAME?.trim() || `cli-${process.pid}`;
|
|
261
263
|
const purpose = runtime.env.KXM_AGENT_PURPOSE?.trim() || "CLI agent client";
|
|
262
264
|
// These commands act as a peer agent, so they take the agent credential: never the
|
|
263
265
|
// persisted admin token, which the hub accepts for any project without its own token.
|
|
264
|
-
const authToken = resolveAgentHubAuthToken(runtime.env, project);
|
|
265
|
-
if (!authToken) throw new AgentProjectTokenMissingError(
|
|
266
|
+
const authToken = resolveAgentHubAuthToken(runtime.env, project, runtime.cwd);
|
|
267
|
+
if (!authToken) throw new AgentProjectTokenMissingError(identity);
|
|
266
268
|
const client = new HubClient({
|
|
267
269
|
serverUrl,
|
|
268
270
|
name,
|
|
@@ -387,11 +389,38 @@ async function dispatchAgentCliCommand(
|
|
|
387
389
|
print(
|
|
388
390
|
runtime.io,
|
|
389
391
|
runtime.json,
|
|
390
|
-
{
|
|
392
|
+
{
|
|
393
|
+
ok: false,
|
|
394
|
+
error: error.code,
|
|
395
|
+
project: error.project,
|
|
396
|
+
projectSource: error.projectSource,
|
|
397
|
+
nextAction: "configure_hub_project",
|
|
398
|
+
hint: error.hint,
|
|
399
|
+
detail: message,
|
|
400
|
+
},
|
|
391
401
|
message,
|
|
392
402
|
);
|
|
393
403
|
return 2;
|
|
394
404
|
}
|
|
405
|
+
if (error instanceof HubHttpError && error.code === "project_token_missing") {
|
|
406
|
+
const identity = resolveProjectIdentity(runtime.cwd, runtime.env);
|
|
407
|
+
const missing = new AgentProjectTokenMissingError(identity);
|
|
408
|
+
print(
|
|
409
|
+
runtime.io,
|
|
410
|
+
runtime.json,
|
|
411
|
+
{
|
|
412
|
+
ok: false,
|
|
413
|
+
error: missing.code,
|
|
414
|
+
project: identity.project,
|
|
415
|
+
projectSource: identity.source,
|
|
416
|
+
nextAction: "configure_hub_project",
|
|
417
|
+
hint: missing.hint,
|
|
418
|
+
detail: missing.message,
|
|
419
|
+
},
|
|
420
|
+
missing.message,
|
|
421
|
+
);
|
|
422
|
+
return 2;
|
|
423
|
+
}
|
|
395
424
|
if (error instanceof CloudTokenError) {
|
|
396
425
|
print(
|
|
397
426
|
runtime.io,
|
|
@@ -1344,7 +1373,8 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
1344
1373
|
.option("--cloud", "Mark the hub remote even when the URL is a loopback forward")
|
|
1345
1374
|
.option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
|
|
1346
1375
|
.option("--token-command <command>", "Program that prints the hub token; the token is not stored")
|
|
1347
|
-
.
|
|
1376
|
+
.option("--key-op <ref>", "op:// reference for the hub token; resolved with op read, never stored")
|
|
1377
|
+
.action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string; keyOp?: string }) {
|
|
1348
1378
|
result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
|
|
1349
1379
|
});
|
|
1350
1380
|
addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));
|
|
@@ -1562,6 +1592,10 @@ export async function runCli(
|
|
|
1562
1592
|
} catch (error) {
|
|
1563
1593
|
if (error instanceof CommanderError) return mapCommanderError(error, argv, program, io);
|
|
1564
1594
|
if (error instanceof DryRunRefused) return error.code;
|
|
1595
|
+
if (error instanceof KxmHubConfigError) {
|
|
1596
|
+
print(io, hasJsonFlag(argv), { ok: false, error: error.code, detail: error.message }, error.message);
|
|
1597
|
+
return 2;
|
|
1598
|
+
}
|
|
1565
1599
|
throw error;
|
|
1566
1600
|
}
|
|
1567
1601
|
}
|