@kontextmind/kxm 0.7.0 → 0.7.4

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 (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/roles/writer.yaml +2 -0
  3. package/docs/README.md +2 -0
  4. package/docs/agent-skills.md +5 -2
  5. package/docs/configuration.md +10 -1
  6. package/docs/getting-started.md +21 -0
  7. package/docs/operations.md +24 -0
  8. package/docs/skills/repo-work-delivery.md +5 -0
  9. package/docs/skills.md +2 -0
  10. package/docs/troubleshooting.md +22 -1
  11. package/package.json +1 -1
  12. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  13. package/plugins/kxm/dist/cli.js +2257 -439
  14. package/plugins/kxm/dist/core.js +57 -0
  15. package/plugins/kxm/dist/extension.js +40 -3
  16. package/plugins/kxm/dist/mcp-server.js +1 -1
  17. package/plugins/kxm/dist/runtime.js +140 -17
  18. package/plugins/kxm/dist/server.js +129 -4
  19. package/plugins/kxm/dist/vnext-runtime-supervisor.js +136 -13
  20. package/plugins/kxm/package.json +1 -1
  21. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
  22. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
  23. package/plugins/kxm/src/autocomplete.ts +1 -1
  24. package/plugins/kxm/src/cli.ts +507 -9
  25. package/plugins/kxm/src/completion-install.ts +223 -0
  26. package/plugins/kxm/src/database.ts +1 -1
  27. package/plugins/kxm/src/external-effects.ts +1 -1
  28. package/plugins/kxm/src/hub-env.ts +193 -0
  29. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  30. package/plugins/kxm/src/local-snapshot.ts +1 -1
  31. package/plugins/kxm/src/mcp-server.ts +1 -1
  32. package/plugins/kxm/src/protocol.ts +111 -0
  33. package/plugins/kxm/src/role.ts +335 -0
  34. package/plugins/kxm/src/safety-integrity.ts +76 -0
  35. package/plugins/kxm/src/sqlite.ts +76 -0
  36. package/plugins/kxm/src/store.ts +1 -1
  37. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  38. package/plugins/kxm/src/vnext-config.ts +38 -1
  39. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  40. package/plugins/kxm/src/vnext-engine.ts +16 -0
  41. package/plugins/kxm/src/vnext-harness.ts +4 -2
  42. package/plugins/kxm/src/vnext-oneshot-process.ts +21 -4
  43. package/plugins/kxm/src/vnext-oneshot-producer.ts +18 -0
  44. package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
  45. package/plugins/kxm/src/workflow-tui.ts +1 -1
  46. package/plugins/kxm/src/workflow.ts +144 -0
  47. package/scripts/kxm-bump-version.mjs +146 -0
  48. package/scripts/kxm-hub.mjs +145 -3
  49. package/scripts/kxm-publish-npm.mjs +3 -1
  50. package/scripts/kxm-release-github.mjs +3 -1
  51. package/scripts/kxm.mjs +0 -0
@@ -15126,6 +15126,19 @@ var OUTPUT_LIMIT = 8 * 1024 * 1024;
15126
15126
  var KILL_GRACE_MS = 250;
15127
15127
  var DRAIN_GRACE_MS = 500;
15128
15128
  var REAP_GRACE_MS = 1e3;
15129
+ function killProcessTree(child, signal = "SIGKILL") {
15130
+ if (process.platform !== "win32" && child.pid) {
15131
+ try {
15132
+ process.kill(-child.pid, signal);
15133
+ return;
15134
+ } catch {
15135
+ }
15136
+ }
15137
+ try {
15138
+ child.kill(signal);
15139
+ } catch {
15140
+ }
15141
+ }
15129
15142
  function defaultSpawn(command, args, options) {
15130
15143
  if (options.signal?.aborted) {
15131
15144
  return Promise.resolve({ stdout: "", stderr: "", code: null, started: false, observedChildExit: false, error: new Error("process_aborted") });
@@ -15162,11 +15175,7 @@ function defaultSpawn(command, args, options) {
15162
15175
  resolve7({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), code, signal, started, observedChildExit, terminationRequested: stopping, ...error ? { error } : {} });
15163
15176
  };
15164
15177
  const kill = (requested) => {
15165
- try {
15166
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, requested);
15167
- else child.kill(requested);
15168
- } catch {
15169
- }
15178
+ killProcessTree(child, requested);
15170
15179
  };
15171
15180
  const stop = (reason) => {
15172
15181
  if (finished) return;
@@ -15493,7 +15502,9 @@ function parseAgyOneShotUsage(stdout, _stderr) {
15493
15502
  var READ_ONLY_ONESHOT_ARGS = Object.freeze({
15494
15503
  claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
15495
15504
  codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
15496
- grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"])
15505
+ grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
15506
+ agy: Object.freeze(["--mode", "plan", "--sandbox", "--disable-slash-commands"]),
15507
+ kimi: Object.freeze(["--plan"])
15497
15508
  });
15498
15509
  function oneShotReadOnlyArgs(harness) {
15499
15510
  return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness] : void 0;
@@ -15547,7 +15558,7 @@ var BUILTIN_HARNESSES = Object.freeze([
15547
15558
  authArgs: ["provider", "list"],
15548
15559
  update: { self: ["upgrade"] },
15549
15560
  oneShot: {
15550
- argv: ["--output-format", "stream-json", "-p"],
15561
+ argv: [...oneShotReadOnlyArgs("kimi"), "--output-format", "stream-json", "-p"],
15551
15562
  promptVia: "arg",
15552
15563
  outputFormat: "stream-json",
15553
15564
  usageParser: parseKimiOneShotUsage
@@ -15610,7 +15621,7 @@ var BUILTIN_HARNESSES = Object.freeze([
15610
15621
  authArgs: ["models"],
15611
15622
  update: { self: ["update"] },
15612
15623
  oneShot: {
15613
- argv: ["--output-format", "json", "-p"],
15624
+ argv: [...oneShotReadOnlyArgs("agy"), "--output-format", "json", "-p"],
15614
15625
  promptVia: "arg",
15615
15626
  outputFormat: "json",
15616
15627
  usageParser: parseAgyOneShotUsage
@@ -16905,7 +16916,7 @@ function validateModelReferences(models, issues) {
16905
16916
  };
16906
16917
  for (const id of models.keys()) walk(id, []);
16907
16918
  }
16908
- function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry) {
16919
+ function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
16909
16920
  const issues = [];
16910
16921
  const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
16911
16922
  const repositoryIds = /* @__PURE__ */ new Set();
@@ -16973,6 +16984,33 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
16973
16984
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
16974
16985
  if (!workflows.has(defaultWorkflow)) issues.push(issue2("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
16975
16986
  for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
16987
+ if (projectRoot) {
16988
+ const writerRolePath = join(projectRoot, ".kxm", "roles", "writer.yaml");
16989
+ const implementerAgent = agents.get("implementer") ?? agents.get("writer");
16990
+ if (existsSync2(writerRolePath) && implementerAgent) {
16991
+ try {
16992
+ const rawRole = parseRestrictedYaml2(readFileSync(writerRolePath, "utf8"));
16993
+ const roleObj = objectValue(rawRole);
16994
+ const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
16995
+ const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m) => Boolean(m));
16996
+ const agentModelObj = objectValue(implementerAgent.value.model);
16997
+ const agentModelStr = stringValue(implementerAgent.value.model);
16998
+ const agentProvider = agentModelObj ? stringValue(agentModelObj.provider) : void 0;
16999
+ const agentModel = agentModelObj ? stringValue(agentModelObj.model) : agentModelStr;
17000
+ const canonicalAgentModel = agentProvider && agentModel ? `${agentProvider}/${agentModel}` : agentModel;
17001
+ if (enabledRosterModels.length > 0 && canonicalAgentModel) {
17002
+ const matches = enabledRosterModels.some((rm) => rm === canonicalAgentModel || rm === agentModel || rm.endsWith(`/${agentModel}`));
17003
+ if (!matches) {
17004
+ issues.push(issue2("semantic", "role_roster_conflicts_with_agent", ".kxm/roles/writer.yaml", `role roster in .kxm/roles/writer.yaml does not include agent model ${canonicalAgentModel} from ${implementerAgent.logicalPath}`));
17005
+ }
17006
+ }
17007
+ } catch (error) {
17008
+ if (error instanceof VnextConfigError) {
17009
+ issues.push(...error.issues);
17010
+ }
17011
+ }
17012
+ }
17013
+ }
16976
17014
  return sortIssues2(issues);
16977
17015
  }
16978
17016
  function vnextCanonicalJson(value) {
@@ -17153,7 +17191,7 @@ function loadVnextProject(projectRoot, options = {}) {
17153
17191
  }
17154
17192
  }
17155
17193
  if (loadIssues.length > 0) throw new VnextConfigError(loadIssues);
17156
- const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry);
17194
+ const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
17157
17195
  if (issues.length > 0) throw new VnextConfigError(issues);
17158
17196
  const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
17159
17197
  return {
@@ -17265,7 +17303,47 @@ import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:pa
17265
17303
 
17266
17304
  // plugins/kxm/src/vnext-bindings.ts
17267
17305
  import { homedir } from "node:os";
17268
- import { DatabaseSync } from "node:sqlite";
17306
+
17307
+ // plugins/kxm/src/sqlite.ts
17308
+ import { createRequire } from "node:module";
17309
+ var requireFromHere = createRequire(import.meta.url);
17310
+ function loadNative() {
17311
+ try {
17312
+ const mod = requireFromHere("node:sqlite");
17313
+ if (mod.DatabaseSync) return { Ctor: mod.DatabaseSync, bun: false };
17314
+ } catch {
17315
+ }
17316
+ try {
17317
+ const mod = requireFromHere("bun:sqlite");
17318
+ const Ctor = mod?.DatabaseSync ?? mod?.Database;
17319
+ if (Ctor) return { Ctor, bun: true };
17320
+ } catch {
17321
+ }
17322
+ throw new Error("kxm: no supported sqlite module found (need node:sqlite or bun:sqlite)");
17323
+ }
17324
+ var native = loadNative();
17325
+ var DatabaseSync = class {
17326
+ inner;
17327
+ constructor(path, options) {
17328
+ let normalized = options;
17329
+ if (native.bun && options) {
17330
+ const { readOnly, ...rest } = options;
17331
+ normalized = readOnly === void 0 ? rest : { ...rest, readonly: readOnly };
17332
+ }
17333
+ this.inner = normalized === void 0 ? new native.Ctor(path) : new native.Ctor(path, normalized);
17334
+ }
17335
+ prepare(sql) {
17336
+ return this.inner.prepare(sql);
17337
+ }
17338
+ exec(sql) {
17339
+ return this.inner.exec(sql);
17340
+ }
17341
+ close() {
17342
+ this.inner.close();
17343
+ }
17344
+ };
17345
+
17346
+ // plugins/kxm/src/vnext-bindings.ts
17269
17347
  import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, parse, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
17270
17348
  var MAX_BINDING_RECORD_BYTES = 256 * 1024;
17271
17349
  var BINDING_LABEL = "Runtime-local repository bindings";
@@ -17308,7 +17386,6 @@ import {
17308
17386
  writeFileSync
17309
17387
  } from "node:fs";
17310
17388
  import { basename as basename2, dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
17311
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
17312
17389
  function databaseError(code, file, message) {
17313
17390
  const issue3 = { phase: "semantic", code, file, message };
17314
17391
  return new VnextConfigError([issue3]);
@@ -17388,7 +17465,7 @@ function openDatabase(file, description, spec) {
17388
17465
  }
17389
17466
  }
17390
17467
  }
17391
- const database = new DatabaseSync2(file);
17468
+ const database = new DatabaseSync(file);
17392
17469
  let transaction = false;
17393
17470
  try {
17394
17471
  database.exec(`PRAGMA busy_timeout = ${spec.timeoutMs ?? 5e3}`);
@@ -22465,6 +22542,32 @@ function evaluateArtifactsGate(context, definition) {
22465
22542
  // plugins/kxm/src/vnext-engine-command.ts
22466
22543
  import { spawn as spawn2 } from "node:child_process";
22467
22544
  import { createHash as createHash8 } from "node:crypto";
22545
+
22546
+ // plugins/kxm/src/safety-integrity.ts
22547
+ var DESTRUCTIVE_COMMAND_PATTERNS = Object.freeze([
22548
+ /^\s*rm\s+.*-[a-zA-Z]*r[a-zA-Z]*f/i,
22549
+ /^\s*rm\s+.*-[a-zA-Z]*f[a-zA-Z]*r/i,
22550
+ /^\s*rm\s+.*(-[a-zA-Z]*r[a-zA-Z]*\s+.*-[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*\s+.*-[a-zA-Z]*r[a-zA-Z]*)/i,
22551
+ /^\s*git\s+reset\s+--hard/i,
22552
+ /^\s*git\s+clean\s+-[a-zA-Z]*f/i,
22553
+ /^\s*git\s+checkout\s+--\s+/i,
22554
+ /^\s*git\s+restore\s+(\.|\*|--staged\s+(\.|\*))/i
22555
+ ]);
22556
+ function assertCommandSeatbelt(command) {
22557
+ for (const pattern of DESTRUCTIVE_COMMAND_PATTERNS) {
22558
+ if (pattern.test(command)) {
22559
+ throw new Error(
22560
+ `Command blocked by KXM safety seatbelt: "${command}". Destructive workspace mutations require explicit operator override.`
22561
+ );
22562
+ }
22563
+ }
22564
+ }
22565
+ var INSECURE_SSH_HOST_KEY_PATTERNS = Object.freeze([
22566
+ /StrictHostKeyChecking=(accept-new|no|off)/i,
22567
+ /UserKnownHostsFile=\/dev\/null/i
22568
+ ]);
22569
+
22570
+ // plugins/kxm/src/vnext-engine-command.ts
22468
22571
  var MAX_DIRECT_TIMER_MS = 2147483647;
22469
22572
  var COMMAND_TERM_GRACE_MS = 2e3;
22470
22573
  var COMMAND_FINAL_WAIT_MS = 2e3;
@@ -22581,6 +22684,7 @@ var CommandObserver = class {
22581
22684
  if (!Number.isInteger(this.definition.timeoutMs) || this.definition.timeoutMs < 1 || this.definition.timeoutMs > MAX_DIRECT_TIMER_MS) {
22582
22685
  throw runtimeError("run_events_illegal", "command", "command timeoutMs exceeds the direct timer bound");
22583
22686
  }
22687
+ assertCommandSeatbelt(this.definition.argv.join(" "));
22584
22688
  this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
22585
22689
  let child;
22586
22690
  try {
@@ -23706,6 +23810,7 @@ async function stepLocked(context, runId, producer, token) {
23706
23810
  return drivePanel(context, prepared.panel, producer);
23707
23811
  }
23708
23812
  async function runPreparedCommandGate(context, prepared, definition, token) {
23813
+ assertCommandSeatbelt(definition.argv.join(" "));
23709
23814
  const storePath = context.eventStore.path;
23710
23815
  const observer = createCommandObserver({
23711
23816
  definition,
@@ -25483,6 +25588,24 @@ function createVnextOneShotProducer(options = {}) {
25483
25588
  "--output-format",
25484
25589
  "json"
25485
25590
  ];
25591
+ } else if (harness === "agy") {
25592
+ args = [
25593
+ "-m",
25594
+ resolved.model,
25595
+ ...permissionArgs,
25596
+ "--output-format",
25597
+ "json",
25598
+ "-p"
25599
+ ];
25600
+ } else if (harness === "kimi") {
25601
+ args = [
25602
+ "-m",
25603
+ resolved.model,
25604
+ ...permissionArgs,
25605
+ "--output-format",
25606
+ "stream-json",
25607
+ "-p"
25608
+ ];
25486
25609
  } else {
25487
25610
  throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
25488
25611
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxm-claude-plugin",
3
- "version": "0.7.0",
3
+ "version": "0.7.4",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,6 +23,15 @@ or backup subcommands.
23
23
 
24
24
  Durable hub SQLite default is `.kxm/state/kxm.db` (`KXM_DATA_PATH`).
25
25
 
26
+ `kxm hub start` requires no token setup on a fresh machine: when
27
+ `KXM_AUTH_TOKEN` is unset, a long random admin token is generated once and
28
+ persisted in `hub-env.json` (schema `kxm.hub-env.v1`, `0600`) under the user
29
+ state root, then reused by every restart, worker, and dashboard. Explicit
30
+ `KXM_AUTH_TOKEN` / `KXM_PROJECT_TOKENS` values win and are persisted too.
31
+ Hub PID claims record the wrapper and server child PID; a dead wrapper's
32
+ claim is reclaimed automatically, an orphaned server is terminated first,
33
+ and `kxm hub stop` recovers such orphans directly.
34
+
26
35
  ```bash
27
36
  kxm hub start
28
37
  kxm hub view --json
@@ -21,15 +21,22 @@ invent `force`, domain-trust, or extra migrate verbs.
21
21
  | `kxm config get <key>` | Get a configuration value | `--json` |
22
22
  | `kxm config set <key> <value>` | Set a configuration value | `--scope user\|project` |
23
23
  | `kxm config list` | List resolved configuration | `--json` |
24
- | `kxm completion <shell>` | Generate completion script | `bash`, `zsh`, or `fish` |
24
+ | `kxm completion install` | Install tab completion for the detected shell and ensure kxm is on `PATH` | `--shell <bash\|zsh\|fish>`, `--no-path`, `--dry-run`, `--json` |
25
25
 
26
26
  ```bash
27
27
  kxm init --dry-run --json
28
28
  kxm migrate plan --json
29
29
  kxm trust diff --base HEAD --json
30
30
  kxm config list --json
31
- kxm completion zsh
31
+ kxm completion install
32
32
  ```
33
33
 
34
+ `kxm completion install` detects the shell from `$SHELL`, writes the
35
+ completion script under the user config directory, appends one idempotent
36
+ stanza to the shell rc file, and adds the kxm bin directory to `PATH` when
37
+ missing. `kxm completion <shell>` (generate only) remains available for
38
+ manual setup. After `kxm init` succeeds in an interactive terminal, kxm
39
+ offers the same install once per shell.
40
+
34
41
  `kxm trust` reviews configuration permission diffs; it does not add website
35
42
  domains. `kxm init` is project-only and does not start the hub.
@@ -55,7 +55,7 @@ const SUBCOMMANDS: Record<string, string[]> = {
55
55
  goal: ["create", "list", "get"],
56
56
  task: ["create", "list", "get", "run", "sync"],
57
57
  studio: ["layout", "serve"],
58
- role: ["list", "get", "add", "remove", "modify"],
58
+ role: ["list", "get", "add", "remove", "modify", "hosts", "set-host", "resume"],
59
59
  };
60
60
 
61
61
  export function generateShellCompletion(shell: SupportedShell): string {