@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/roles/writer.yaml +2 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +5 -2
- package/docs/configuration.md +10 -1
- package/docs/getting-started.md +21 -0
- package/docs/operations.md +24 -0
- package/docs/skills/repo-work-delivery.md +5 -0
- package/docs/skills.md +2 -0
- package/docs/troubleshooting.md +22 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2257 -439
- package/plugins/kxm/dist/core.js +57 -0
- package/plugins/kxm/dist/extension.js +40 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +140 -17
- package/plugins/kxm/dist/server.js +129 -4
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +136 -13
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/cli.ts +507 -9
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/external-effects.ts +1 -1
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/role.ts +335 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +38 -1
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +16 -0
- package/plugins/kxm/src/vnext-harness.ts +4 -2
- package/plugins/kxm/src/vnext-oneshot-process.ts +21 -4
- package/plugins/kxm/src/vnext-oneshot-producer.ts +18 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
- package/plugins/kxm/src/workflow-tui.ts +1 -1
- package/plugins/kxm/src/workflow.ts +144 -0
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +145 -3
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
|
@@ -195,6 +195,24 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
195
195
|
args = ["--model", resolved.model,
|
|
196
196
|
...(resolved.thinking ? ["--reasoning-effort", resolved.thinking] : []),
|
|
197
197
|
...permissionArgs, "--output-format", "json"];
|
|
198
|
+
} else if (harness === "agy") {
|
|
199
|
+
args = [
|
|
200
|
+
"-m",
|
|
201
|
+
resolved.model,
|
|
202
|
+
...permissionArgs,
|
|
203
|
+
"--output-format",
|
|
204
|
+
"json",
|
|
205
|
+
"-p",
|
|
206
|
+
];
|
|
207
|
+
} else if (harness === "kimi") {
|
|
208
|
+
args = [
|
|
209
|
+
"-m",
|
|
210
|
+
resolved.model,
|
|
211
|
+
...permissionArgs,
|
|
212
|
+
"--output-format",
|
|
213
|
+
"stream-json",
|
|
214
|
+
"-p",
|
|
215
|
+
];
|
|
198
216
|
} else {
|
|
199
217
|
throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
|
|
200
218
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "
|
|
4
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
5
5
|
import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
|
|
6
6
|
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
7
7
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { DatabaseSync } from "
|
|
8
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
9
9
|
import { repoConfigDirectory } from "./config.ts";
|
|
10
10
|
import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
11
11
|
|
|
@@ -3,11 +3,17 @@ import {
|
|
|
3
3
|
MAX_MESSAGE_TTL_MS,
|
|
4
4
|
MIN_MESSAGE_TTL_MS,
|
|
5
5
|
ProtocolError,
|
|
6
|
+
TERMINAL_RECEIPT_SCHEMA,
|
|
6
7
|
newId,
|
|
7
8
|
requireString,
|
|
9
|
+
validateTerminalReceipt,
|
|
8
10
|
type ImprovementArea,
|
|
9
11
|
type JournalCategory,
|
|
10
12
|
type MessageRecord,
|
|
13
|
+
type TerminalReceipt,
|
|
14
|
+
type TerminalReceiptEvidence,
|
|
15
|
+
type TerminalReceiptMetrics,
|
|
16
|
+
type TerminalReceiptStatus,
|
|
11
17
|
type WorkflowCheckpointStatus,
|
|
12
18
|
type WorkflowEvidenceInput,
|
|
13
19
|
type WorkflowEvidenceReference,
|
|
@@ -18,11 +24,16 @@ import {
|
|
|
18
24
|
export type {
|
|
19
25
|
ImprovementArea,
|
|
20
26
|
JournalCategory,
|
|
27
|
+
TerminalReceipt,
|
|
28
|
+
TerminalReceiptEvidence,
|
|
29
|
+
TerminalReceiptMetrics,
|
|
30
|
+
TerminalReceiptStatus,
|
|
21
31
|
WorkflowCheckpointStatus,
|
|
22
32
|
WorkflowEvidenceInput,
|
|
23
33
|
WorkflowEvidenceReference,
|
|
24
34
|
WorkflowEvidenceReferenceInput,
|
|
25
35
|
} from "./protocol.ts";
|
|
36
|
+
export { TERMINAL_RECEIPT_SCHEMA, validateTerminalReceipt } from "./protocol.ts";
|
|
26
37
|
|
|
27
38
|
export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed";
|
|
28
39
|
export type WorkflowStageStatus = "pending" | "in_progress" | "waiting" | WorkflowCheckpointStatus;
|
|
@@ -153,6 +164,8 @@ export interface WorkflowStageDefinition {
|
|
|
153
164
|
instructions: string;
|
|
154
165
|
requiredEvidence: string[];
|
|
155
166
|
maxAttempts: number;
|
|
167
|
+
/** Bounded in-place retry limit before audit escalation (default: 2). */
|
|
168
|
+
autoResumeLimit?: number;
|
|
156
169
|
area?: ImprovementArea;
|
|
157
170
|
evidencePolicies?: WorkflowEvidencePolicies;
|
|
158
171
|
/** Typed outcome map (v0.5). Keys are outcome identities ("passed",
|
|
@@ -250,6 +263,13 @@ export interface WorkflowStageState extends WorkflowStageDefinition {
|
|
|
250
263
|
startedAt?: string;
|
|
251
264
|
completedAt?: string;
|
|
252
265
|
updatedAt?: string;
|
|
266
|
+
receipt?: TerminalReceipt;
|
|
267
|
+
auditEscalation?: {
|
|
268
|
+
reason: string;
|
|
269
|
+
timestamp: string;
|
|
270
|
+
ruling?: string;
|
|
271
|
+
receipt?: TerminalReceipt;
|
|
272
|
+
};
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
export interface CapturedOracle {
|
|
@@ -1003,12 +1023,18 @@ export function parseWorkflowDefinitions(
|
|
|
1003
1023
|
&& (!Number.isInteger(stageMaxTransitions) || (stageMaxTransitions as number) < 1 || (stageMaxTransitions as number) > 100)) {
|
|
1004
1024
|
throw new Error(`stage ${stageId} maxTransitions must be an integer between 1 and 100`);
|
|
1005
1025
|
}
|
|
1026
|
+
const autoResumeLimit = stage.autoResumeLimit;
|
|
1027
|
+
if (autoResumeLimit !== undefined
|
|
1028
|
+
&& (!Number.isInteger(autoResumeLimit) || (autoResumeLimit as number) < 1 || (autoResumeLimit as number) > 20)) {
|
|
1029
|
+
throw new Error(`stage ${stageId} autoResumeLimit must be an integer between 1 and 20`);
|
|
1030
|
+
}
|
|
1006
1031
|
return {
|
|
1007
1032
|
id: stageId,
|
|
1008
1033
|
label: requireString(stage.label ?? stageId, "stage.label", { max: 128 }),
|
|
1009
1034
|
instructions: requireString(stage.instructions, "stage.instructions", { max: 4_000 }),
|
|
1010
1035
|
requiredEvidence,
|
|
1011
1036
|
maxAttempts: maxAttempts as number,
|
|
1037
|
+
...(autoResumeLimit !== undefined ? { autoResumeLimit: autoResumeLimit as number } : {}),
|
|
1012
1038
|
...(area ? { area } : {}),
|
|
1013
1039
|
...(evidencePolicies ? { evidencePolicies } : {}),
|
|
1014
1040
|
...(on ? { on } : {}),
|
|
@@ -1377,6 +1403,31 @@ export function checkpointRun(
|
|
|
1377
1403
|
delete run.currentStage;
|
|
1378
1404
|
return { retry: false, completed: false, run };
|
|
1379
1405
|
}
|
|
1406
|
+
if (stage.autoResumeLimit !== undefined && stage.attempts >= stage.autoResumeLimit) {
|
|
1407
|
+
stage.status = "in_progress";
|
|
1408
|
+
const reason = summary || `autoResumeLimit of ${stage.autoResumeLimit} reached on stage ${stage.id}`;
|
|
1409
|
+
const receipt = validateTerminalReceipt({
|
|
1410
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1411
|
+
status: "audit_escalation",
|
|
1412
|
+
seat: stage.id,
|
|
1413
|
+
runId: run.id,
|
|
1414
|
+
stageId: stage.id,
|
|
1415
|
+
timestamp,
|
|
1416
|
+
host: "pi",
|
|
1417
|
+
model: "default",
|
|
1418
|
+
escalationReason: reason,
|
|
1419
|
+
});
|
|
1420
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1421
|
+
waitForWorkflowSignal(run, stage.id, "audit_escalation", reason, timestamp, expiresAt);
|
|
1422
|
+
stage.receipt = receipt;
|
|
1423
|
+
stage.auditEscalation = {
|
|
1424
|
+
reason,
|
|
1425
|
+
timestamp,
|
|
1426
|
+
receipt,
|
|
1427
|
+
};
|
|
1428
|
+
return { retry: false, completed: false, run };
|
|
1429
|
+
}
|
|
1430
|
+
stage.status = status;
|
|
1380
1431
|
// Declared failure outcomes create typed transitions (bounded); the
|
|
1381
1432
|
// outcome identity defaults to the checkpoint status.
|
|
1382
1433
|
const outcomeKey = outcome ?? status;
|
|
@@ -1546,3 +1597,96 @@ export function approveWorkflowDegradation(
|
|
|
1546
1597
|
run.updatedAt = timestamp;
|
|
1547
1598
|
return { run, approval, created: true };
|
|
1548
1599
|
}
|
|
1600
|
+
|
|
1601
|
+
export function escalateWorkflowStage(
|
|
1602
|
+
run: WorkflowRun,
|
|
1603
|
+
stageId: string,
|
|
1604
|
+
reason: string,
|
|
1605
|
+
timestamp: string,
|
|
1606
|
+
options: {
|
|
1607
|
+
seat?: string;
|
|
1608
|
+
host?: string;
|
|
1609
|
+
model?: string;
|
|
1610
|
+
evidence?: TerminalReceiptEvidence;
|
|
1611
|
+
metrics?: TerminalReceiptMetrics;
|
|
1612
|
+
} = {},
|
|
1613
|
+
): { run: WorkflowRun; receipt: TerminalReceipt } {
|
|
1614
|
+
if (run.status !== "running") throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_terminal");
|
|
1615
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1616
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1617
|
+
if (stage.id !== run.currentStage || stage.status !== "in_progress") {
|
|
1618
|
+
throw new ProtocolError(409, `stage ${stageId} is not currently active`, "workflow_stage_out_of_order");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const receipt: TerminalReceipt = validateTerminalReceipt({
|
|
1622
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1623
|
+
status: "audit_escalation",
|
|
1624
|
+
seat: options.seat ?? stage.id,
|
|
1625
|
+
runId: run.id,
|
|
1626
|
+
stageId,
|
|
1627
|
+
timestamp,
|
|
1628
|
+
host: options.host ?? "pi",
|
|
1629
|
+
model: options.model ?? "default",
|
|
1630
|
+
escalationReason: reason,
|
|
1631
|
+
...(options.evidence ? { evidence: options.evidence } : {}),
|
|
1632
|
+
...(options.metrics ? { metrics: options.metrics } : {}),
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1636
|
+
waitForWorkflowSignal(run, stageId, "audit_escalation", reason, timestamp, expiresAt);
|
|
1637
|
+
|
|
1638
|
+
stage.receipt = receipt;
|
|
1639
|
+
stage.auditEscalation = {
|
|
1640
|
+
reason,
|
|
1641
|
+
timestamp,
|
|
1642
|
+
receipt,
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
return { run, receipt };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export function resumeWorkflowFromRuling(
|
|
1649
|
+
run: WorkflowRun,
|
|
1650
|
+
ruling: string,
|
|
1651
|
+
timestamp: string,
|
|
1652
|
+
options: {
|
|
1653
|
+
status?: WorkflowCheckpointStatus;
|
|
1654
|
+
evidence?: WorkflowEvidenceInput;
|
|
1655
|
+
} = {},
|
|
1656
|
+
): { retry: boolean; completed: boolean; run: WorkflowRun; stageId: string } {
|
|
1657
|
+
if (run.status !== "waiting" || !run.waiting) {
|
|
1658
|
+
throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_not_waiting");
|
|
1659
|
+
}
|
|
1660
|
+
if (run.waiting.signalKey !== "audit_escalation") {
|
|
1661
|
+
throw new ProtocolError(
|
|
1662
|
+
409,
|
|
1663
|
+
`workflow is waiting for signal '${run.waiting.signalKey}', not 'audit_escalation'`,
|
|
1664
|
+
"workflow_signal_mismatch",
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const stageId = run.waiting.stageId;
|
|
1669
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1670
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1671
|
+
|
|
1672
|
+
const status = options.status ?? "passed";
|
|
1673
|
+
const evidence = options.evidence ?? {};
|
|
1674
|
+
|
|
1675
|
+
if (stage.auditEscalation) {
|
|
1676
|
+
stage.auditEscalation.ruling = ruling;
|
|
1677
|
+
if (stage.auditEscalation.receipt) {
|
|
1678
|
+
stage.auditEscalation.receipt.ruling = ruling;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
stage.summary = `Resumed by operator ruling: ${ruling}`;
|
|
1682
|
+
|
|
1683
|
+
return resumeWorkflowFromSignal(
|
|
1684
|
+
run,
|
|
1685
|
+
"audit_escalation",
|
|
1686
|
+
status,
|
|
1687
|
+
`Resumed: ${ruling}`,
|
|
1688
|
+
evidence,
|
|
1689
|
+
timestamp,
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bump the package version across all tracked surfaces.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/kxm-bump-version.mjs <new-version> # explicit version
|
|
8
|
+
* node scripts/kxm-bump-version.mjs patch # bump patch
|
|
9
|
+
* node scripts/kxm-bump-version.mjs minor # bump minor
|
|
10
|
+
* node scripts/kxm-bump-version.mjs major # bump major
|
|
11
|
+
*
|
|
12
|
+
* Surfaces updated (must stay in sync with scripts/check-versions.mjs):
|
|
13
|
+
* 1. package.json .version
|
|
14
|
+
* 2. package-lock.json .version + .packages[""].version
|
|
15
|
+
* 3. plugins/kxm/package.json .version
|
|
16
|
+
* 4. plugins/kxm/.claude-plugin/plugin.json .version
|
|
17
|
+
* 5. .claude-plugin/marketplace.json .plugins[name=kxm].version
|
|
18
|
+
* 6. plugins/kxm/src/mcp-server.ts const VERSION = "..."
|
|
19
|
+
* 7. plugins/kxm/dist/mcp-server.js const VERSION = "..." (rebuilt)
|
|
20
|
+
*
|
|
21
|
+
* After running this script, run `npm run build` to rebuild dist bundles.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
|
|
29
|
+
|
|
30
|
+
export function parseSemver(version) {
|
|
31
|
+
const match = SEMVER_RE.exec(version);
|
|
32
|
+
if (!match) return undefined;
|
|
33
|
+
return {
|
|
34
|
+
major: Number(match[1]),
|
|
35
|
+
minor: Number(match[2]),
|
|
36
|
+
patch: Number(match[3]),
|
|
37
|
+
prerelease: match[4],
|
|
38
|
+
build: match[5],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function bumpVersion(current, kind) {
|
|
43
|
+
const parsed = parseSemver(current);
|
|
44
|
+
if (!parsed) throw new Error(`invalid current version: ${current}`);
|
|
45
|
+
switch (kind) {
|
|
46
|
+
case "major":
|
|
47
|
+
return `${parsed.major + 1}.0.0`;
|
|
48
|
+
case "minor":
|
|
49
|
+
return `${parsed.major}.${parsed.minor + 1}.0`;
|
|
50
|
+
case "patch":
|
|
51
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`unknown bump kind: ${kind} (use major, minor, or patch)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Replace a version string in a JSON file using targeted string patching. */
|
|
58
|
+
function patchJsonVersion(filePath, oldVersion, newVersion) {
|
|
59
|
+
const raw = readFileSync(filePath, "utf8");
|
|
60
|
+
const escaped = oldVersion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
const pattern = new RegExp(`("version"\\s*:\\s*)"${escaped}"`, "g");
|
|
62
|
+
const patched = raw.replace(pattern, `$1"${newVersion}"`);
|
|
63
|
+
if (patched === raw) {
|
|
64
|
+
throw new Error(`no version field found to patch in ${filePath}`);
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(filePath, patched, "utf8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Replace a version string in a TypeScript/JavaScript source file. */
|
|
70
|
+
function patchSourceVersion(filePath, newVersion) {
|
|
71
|
+
const raw = readFileSync(filePath, "utf8");
|
|
72
|
+
const patched = raw.replace(
|
|
73
|
+
/const VERSION = "[^"]+"/,
|
|
74
|
+
`const VERSION = "${newVersion}"`,
|
|
75
|
+
);
|
|
76
|
+
if (patched === raw) {
|
|
77
|
+
throw new Error(`no VERSION constant found in ${filePath}`);
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(filePath, patched, "utf8");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function applyVersionBump(root, newVersion) {
|
|
83
|
+
if (!parseSemver(newVersion)) {
|
|
84
|
+
throw new Error(`invalid semver: ${newVersion}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Read current version
|
|
88
|
+
const pkgPath = resolve(root, "package.json");
|
|
89
|
+
const oldVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
90
|
+
|
|
91
|
+
// JSON surfaces (targeted patch preserves formatting)
|
|
92
|
+
const jsonFiles = [
|
|
93
|
+
"package.json",
|
|
94
|
+
"package-lock.json",
|
|
95
|
+
"plugins/kxm/package.json",
|
|
96
|
+
"plugins/kxm/.claude-plugin/plugin.json",
|
|
97
|
+
".claude-plugin/marketplace.json",
|
|
98
|
+
];
|
|
99
|
+
for (const rel of jsonFiles) {
|
|
100
|
+
patchJsonVersion(resolve(root, rel), oldVersion, newVersion);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// TypeScript source
|
|
104
|
+
patchSourceVersion(resolve(root, "plugins/kxm/src/mcp-server.ts"), newVersion);
|
|
105
|
+
|
|
106
|
+
// Generated dist (update in place if it exists; check:generated rebuilds anyway)
|
|
107
|
+
const distPath = resolve(root, "plugins/kxm/dist/mcp-server.js");
|
|
108
|
+
try {
|
|
109
|
+
patchSourceVersion(distPath, newVersion);
|
|
110
|
+
} catch {
|
|
111
|
+
// dist may not exist yet; will be rebuilt by npm run build
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { oldVersion, newVersion };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function main(argv = process.argv.slice(2), root = process.cwd()) {
|
|
118
|
+
const arg = argv[0];
|
|
119
|
+
if (!arg) {
|
|
120
|
+
process.stderr.write("usage: node scripts/kxm-bump-version.mjs <version|patch|minor|major>\n");
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pkgPath = resolve(root, "package.json");
|
|
125
|
+
const currentVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
126
|
+
|
|
127
|
+
let newVersion;
|
|
128
|
+
if (SEMVER_RE.test(arg)) {
|
|
129
|
+
newVersion = arg;
|
|
130
|
+
} else {
|
|
131
|
+
newVersion = bumpVersion(currentVersion, arg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (newVersion === currentVersion) {
|
|
135
|
+
process.stdout.write(`version unchanged: ${currentVersion}\n`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = applyVersionBump(root, newVersion);
|
|
140
|
+
process.stdout.write(`bumped ${result.oldVersion} -> ${result.newVersion}\n`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
145
|
+
process.exitCode = main();
|
|
146
|
+
}
|
package/scripts/kxm-hub.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
5
7
|
import { join, resolve } from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
|
|
@@ -15,6 +17,19 @@ const startedAt = new Date().toISOString();
|
|
|
15
17
|
const controlFile = "hub.stop";
|
|
16
18
|
const controlPath = join(stateDir, controlFile);
|
|
17
19
|
|
|
20
|
+
// --- Persistent hub credentials (kxm.hub-env.v1) -------------------------
|
|
21
|
+
// Precedence: KXM_AUTH_TOKEN/KXM_PROJECT_TOKENS env, then the persisted
|
|
22
|
+
// user-state file, then a generated admin token that is persisted so later
|
|
23
|
+
// restarts, workers, and dashboards reuse the same credential.
|
|
24
|
+
function resolveUserStateRoot() {
|
|
25
|
+
const explicit = process.env.KXM_STATE_HOME?.trim();
|
|
26
|
+
if (explicit) return resolve(explicit);
|
|
27
|
+
const home = homedir();
|
|
28
|
+
if (process.platform === "win32") return resolve(process.env.LOCALAPPDATA?.trim() || join(home, "AppData", "Local"), "KXM");
|
|
29
|
+
if (process.platform === "darwin") return resolve(home, "Library", "Application Support", "KXM");
|
|
30
|
+
return resolve(process.env.XDG_STATE_HOME?.trim() || join(home, ".local", "state"), "kxm");
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
function processExists(pid) {
|
|
19
34
|
try {
|
|
20
35
|
process.kill(pid, 0);
|
|
@@ -24,6 +39,119 @@ function processExists(pid) {
|
|
|
24
39
|
}
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
function parseHubEnvFile() {
|
|
43
|
+
const file = join(resolveUserStateRoot(), "hub-env.json");
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = readFileSync(file, "utf8");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.code === "ENOENT") return { file, record: undefined };
|
|
49
|
+
throw new Error(`KXM hub could not read the persisted hub env at ${file}: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`KXM hub env file is malformed at ${file}; fix or remove it (it holds only KXM_AUTH_TOKEN/KXM_PROJECT_TOKENS values): ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.schema !== "kxm.hub-env.v1") {
|
|
58
|
+
throw new Error(`KXM hub env file at ${file} does not use schema kxm.hub-env.v1`);
|
|
59
|
+
}
|
|
60
|
+
return { file, record: parsed };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generateAdminToken() {
|
|
64
|
+
return `kxm_admin_${randomBytes(24).toString("base64url")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function persistHubEnv(file, record) {
|
|
68
|
+
mkdirSync(resolve(file, ".."), { recursive: true });
|
|
69
|
+
const temporary = join(resolve(file, ".."), `.hub-env-${process.pid}.tmp`);
|
|
70
|
+
writeFileSync(temporary, `${JSON.stringify(record, undefined, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
71
|
+
try {
|
|
72
|
+
renameSync(temporary, file);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
rmSync(temporary, { force: true });
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseProjectTokensEnv(raw) {
|
|
80
|
+
const trimmed = raw?.trim();
|
|
81
|
+
if (!trimmed) return undefined;
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(trimmed);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error("KXM_PROJECT_TOKENS must be a JSON object of project names to tokens");
|
|
87
|
+
}
|
|
88
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
89
|
+
throw new Error("KXM_PROJECT_TOKENS must be a JSON object of project names to tokens");
|
|
90
|
+
}
|
|
91
|
+
for (const [project, token] of Object.entries(parsed)) {
|
|
92
|
+
if (!project.trim() || typeof token !== "string" || !token.trim()) {
|
|
93
|
+
throw new Error("KXM_PROJECT_TOKENS must contain non-empty project names and token strings");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Resolve credentials, persisting generated/env values into the user-state file. */
|
|
100
|
+
function resolveCredentials() {
|
|
101
|
+
const { file, record } = parseHubEnvFile();
|
|
102
|
+
const envToken = process.env.KXM_AUTH_TOKEN?.trim();
|
|
103
|
+
const envProjectTokens = parseProjectTokensEnv(process.env.KXM_PROJECT_TOKENS);
|
|
104
|
+
let authToken = envToken || record?.authToken;
|
|
105
|
+
let authTokenSource = envToken ? "env" : record?.authToken ? "file" : "none";
|
|
106
|
+
if (!authToken) {
|
|
107
|
+
authToken = generateAdminToken();
|
|
108
|
+
authTokenSource = "generated";
|
|
109
|
+
}
|
|
110
|
+
const projectTokens = envProjectTokens || record?.projectTokens;
|
|
111
|
+
const projectTokensSource = envProjectTokens ? "env" : record?.projectTokens ? "file" : "none";
|
|
112
|
+
const envChanged = (authTokenSource === "env" && record?.authToken !== authToken)
|
|
113
|
+
|| (projectTokensSource === "env" && JSON.stringify(record?.projectTokens ?? undefined) !== JSON.stringify(projectTokens ?? undefined));
|
|
114
|
+
if (authTokenSource === "generated" || envChanged) {
|
|
115
|
+
persistHubEnv(file, {
|
|
116
|
+
schema: "kxm.hub-env.v1",
|
|
117
|
+
createdAt: record?.createdAt || new Date().toISOString(),
|
|
118
|
+
...(authToken ? { authToken } : {}),
|
|
119
|
+
...(projectTokens ? { projectTokens } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return { authToken, authTokenSource, projectTokens, projectTokensSource, file };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let credentials;
|
|
126
|
+
try {
|
|
127
|
+
credentials = resolveCredentials();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
process.stderr.write(`kxm hub: ${error.message}\n`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const launchEnv = { ...process.env };
|
|
133
|
+
if (credentials.authToken) launchEnv.KXM_AUTH_TOKEN = credentials.authToken;
|
|
134
|
+
if (credentials.projectTokens) launchEnv.KXM_PROJECT_TOKENS = JSON.stringify(credentials.projectTokens);
|
|
135
|
+
if (credentials.authTokenSource !== "env") {
|
|
136
|
+
process.stdout.write(`kxm hub: using ${credentials.authTokenSource === "generated" ? "newly generated" : "persisted"} KXM_AUTH_TOKEN from ${credentials.file}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function terminateTree(pid) {
|
|
140
|
+
// Best-effort cleanup of an orphaned hub server whose wrapper died
|
|
141
|
+
// without reclaiming (e.g. SIGKILL). The wrapper could not be stopped
|
|
142
|
+
// gracefully in that state, so the server is asked to shut down first and
|
|
143
|
+
// forced after a short grace period.
|
|
144
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
145
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* already gone */ }
|
|
146
|
+
const deadline = Date.now() + 3000;
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
if (!processExists(pid)) return;
|
|
149
|
+
const wait = Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
150
|
+
if (wait !== "timed-out") return;
|
|
151
|
+
}
|
|
152
|
+
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
27
155
|
function claimPidFile() {
|
|
28
156
|
const record = { version: 1, pid: process.pid, role: "hub", startedAt, controlFile };
|
|
29
157
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -50,9 +178,16 @@ function claimPidFile() {
|
|
|
50
178
|
if (existing?.version === 1 && existing.role === "hub" && Number.isInteger(existing.pid) && processExists(existing.pid)) {
|
|
51
179
|
throw new Error(`KXM hub is already managed by PID ${existing.pid}`);
|
|
52
180
|
}
|
|
53
|
-
// A well-formed claim whose
|
|
181
|
+
// A well-formed claim whose wrapper is dead is safe to reclaim. Keep
|
|
54
182
|
// malformed claims fail-closed because they may belong to another tool.
|
|
55
183
|
if (existing?.version === 1 && existing.role === "hub" && Number.isInteger(existing.pid) && !processExists(existing.pid)) {
|
|
184
|
+
// The wrapper is gone. An orphaned server child recorded by the dead
|
|
185
|
+
// wrapper is cleaned up before reclaiming so a SIGKILLed wrapper does
|
|
186
|
+
// not leave a port-holding server behind.
|
|
187
|
+
if (Number.isInteger(existing.serverPid) && existing.serverPid !== existing.pid && processExists(existing.serverPid)) {
|
|
188
|
+
process.stderr.write(`kxm hub: cleaning up orphaned hub server PID ${existing.serverPid} from a dead wrapper\n`);
|
|
189
|
+
terminateTree(existing.serverPid);
|
|
190
|
+
}
|
|
56
191
|
rmSync(pidPath, { force: true });
|
|
57
192
|
continue;
|
|
58
193
|
}
|
|
@@ -79,8 +214,15 @@ const child = spawn(process.execPath, [
|
|
|
79
214
|
...process.argv.slice(2),
|
|
80
215
|
], {
|
|
81
216
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
82
|
-
env:
|
|
217
|
+
env: launchEnv,
|
|
83
218
|
});
|
|
219
|
+
// Record the server child PID inside the claim so a later wrapper can clean
|
|
220
|
+
// up an orphaned server if this wrapper is killed without running cleanupPid.
|
|
221
|
+
if (child.pid) {
|
|
222
|
+
try {
|
|
223
|
+
writeFileSync(pidPath, `${JSON.stringify({ version: 1, pid: process.pid, serverPid: child.pid, role: "hub", startedAt, controlFile })}\n`, "utf8");
|
|
224
|
+
} catch { /* claim already written; serverPid tracking is best-effort */ }
|
|
225
|
+
}
|
|
84
226
|
let stopping = false;
|
|
85
227
|
|
|
86
228
|
child.once("error", (error) => {
|
|
@@ -275,7 +275,9 @@ export async function main(env = process.env, stdout = process.stdout, stderr =
|
|
|
275
275
|
const token = env.GITHUB_TOKEN || env.GH_TOKEN;
|
|
276
276
|
const npmToken = env.NODE_AUTH_TOKEN || env.NPM_TOKEN;
|
|
277
277
|
const repo = env.GITHUB_REPOSITORY;
|
|
278
|
-
|
|
278
|
+
// KXM_RELEASE_TAG is explicit for workflow_dispatch runs; GITHUB_REF_NAME
|
|
279
|
+
// is the branch name for that event, not the requested release tag.
|
|
280
|
+
const tag = env.KXM_RELEASE_TAG || env.TAG || env.GITHUB_REF_NAME;
|
|
279
281
|
const assetName = env.KXM_ASSET;
|
|
280
282
|
const assetPath = env.KXM_ASSET_PATH;
|
|
281
283
|
const declaredSha256 = env.KXM_ASSET_SHA256;
|
|
@@ -427,7 +427,9 @@ export function fileSha256(path) {
|
|
|
427
427
|
export async function main(env = process.env, stdout = process.stdout, stderr = process.stderr) {
|
|
428
428
|
const token = env.GITHUB_TOKEN || env.GH_TOKEN;
|
|
429
429
|
const repo = env.GITHUB_REPOSITORY;
|
|
430
|
-
|
|
430
|
+
// KXM_RELEASE_TAG is explicit for workflow_dispatch runs; GITHUB_REF_NAME
|
|
431
|
+
// is the branch name for that event, not the requested release tag.
|
|
432
|
+
const tag = env.KXM_RELEASE_TAG || env.TAG || env.GITHUB_REF_NAME;
|
|
431
433
|
const assetName = env.KXM_ASSET;
|
|
432
434
|
const assetPath = env.KXM_ASSET_PATH;
|
|
433
435
|
const declared = env.KXM_ASSET_SHA256;
|
package/scripts/kxm.mjs
CHANGED
|
File without changes
|