@kontextmind/kxm 0.6.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.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,238 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ export const KXM_UPDATE_SCHEMA = "kxm.update.v1" as const;
6
+ export const KXM_UPDATE_CACHE = "update-check.json";
7
+ const CHECK_TIMEOUT_MS = 2_500;
8
+ const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
9
+ const SHA256_DIGEST = /^sha256:([0-9a-fA-F]{64})$/;
10
+
11
+ export type KxmUpdateSource = "npm" | "github";
12
+
13
+ export interface KxmReleaseAsset {
14
+ name: string;
15
+ sha256: string;
16
+ }
17
+
18
+ export interface KxmUpdateConfig {
19
+ schema: typeof KXM_UPDATE_SCHEMA;
20
+ auto: boolean;
21
+ source: KxmUpdateSource;
22
+ }
23
+
24
+ export interface KxmUpdateNotice {
25
+ current: string;
26
+ latest?: string;
27
+ available: boolean;
28
+ auto: boolean;
29
+ source: KxmUpdateSource;
30
+ message: string;
31
+ asset?: KxmReleaseAsset;
32
+ }
33
+
34
+ export class KxmUpdateConfigError extends Error {
35
+ readonly code = "kxm_update_config_invalid" as const;
36
+ constructor(message: string) {
37
+ super(message);
38
+ this.name = "KxmUpdateConfigError";
39
+ }
40
+ }
41
+
42
+ export function kxmReleaseAssetName(version: string): string {
43
+ return `kxm-${version}.tgz`;
44
+ }
45
+
46
+ export function readInstalledKxmVersion(root: string): string {
47
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: unknown };
48
+ if (typeof pkg.version !== "string" || !parseSemver(pkg.version)) {
49
+ throw new KxmUpdateConfigError("package.json version is missing or not a semver");
50
+ }
51
+ return pkg.version;
52
+ }
53
+
54
+ export function parseSemver(value: string): [number, number, number] | undefined {
55
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
56
+ if (!match) return undefined;
57
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
58
+ }
59
+
60
+ export function compareSemver(left: string, right: string): number | undefined {
61
+ const a = parseSemver(left);
62
+ const b = parseSemver(right);
63
+ if (!a || !b) return undefined;
64
+ for (let i = 0; i < 3; i += 1) {
65
+ if (a[i]! !== b[i]!) return a[i]! < b[i]! ? -1 : 1;
66
+ }
67
+ return 0;
68
+ }
69
+
70
+ export function formatKxmUpdateNotice(notice: KxmUpdateNotice): string {
71
+ if (!notice.available) return notice.message;
72
+ const apply = notice.auto ? "kxm update --kxm (auto)" : "kxm update --kxm";
73
+ return `kxm ${notice.current} → ${notice.latest} available · ${apply}`;
74
+ }
75
+
76
+ function withAsset(notice: KxmUpdateNotice, asset?: KxmReleaseAsset): KxmUpdateNotice {
77
+ return asset ? { ...notice, asset } : notice;
78
+ }
79
+
80
+ export function noticeFromVersions(
81
+ current: string,
82
+ latest: string | undefined,
83
+ config: KxmUpdateConfig,
84
+ checkError?: string,
85
+ asset?: KxmReleaseAsset,
86
+ ): KxmUpdateNotice {
87
+ if (checkError || !latest) {
88
+ return withAsset({
89
+ current,
90
+ auto: config.auto,
91
+ source: config.source,
92
+ available: false,
93
+ message: checkError ? `kxm update check unavailable (${checkError})` : `kxm ${current}`,
94
+ }, asset);
95
+ }
96
+ const cmp = compareSemver(current, latest);
97
+ if (cmp === undefined) {
98
+ return withAsset({
99
+ current,
100
+ latest,
101
+ auto: config.auto,
102
+ source: config.source,
103
+ available: false,
104
+ message: "kxm update check unavailable (non-semver)",
105
+ }, asset);
106
+ }
107
+ if (cmp >= 0) {
108
+ return withAsset({
109
+ current,
110
+ latest,
111
+ auto: config.auto,
112
+ source: config.source,
113
+ available: false,
114
+ message: `kxm ${current}`,
115
+ }, asset);
116
+ }
117
+ const available: KxmUpdateNotice = {
118
+ current,
119
+ latest,
120
+ auto: config.auto,
121
+ source: config.source,
122
+ available: true,
123
+ message: "",
124
+ ...(asset ? { asset } : {}),
125
+ };
126
+ available.message = formatKxmUpdateNotice(available);
127
+ return available;
128
+ }
129
+
130
+ export function readUpdateCache(stateDir: string, now = Date.now()): KxmUpdateNotice | undefined {
131
+ const path = join(stateDir, KXM_UPDATE_CACHE);
132
+ if (!existsSync(path)) return undefined;
133
+ try {
134
+ const row = JSON.parse(readFileSync(path, "utf8")) as {
135
+ checkedAt?: unknown;
136
+ notice?: KxmUpdateNotice;
137
+ };
138
+ if (typeof row.checkedAt !== "number" || !row.notice || now - row.checkedAt > CACHE_TTL_MS) return undefined;
139
+ if (typeof row.notice.current !== "string" || typeof row.notice.available !== "boolean") return undefined;
140
+ return row.notice;
141
+ } catch {
142
+ return undefined;
143
+ }
144
+ }
145
+
146
+ export function writeUpdateCache(stateDir: string, notice: KxmUpdateNotice, now = Date.now()): void {
147
+ mkdirSync(stateDir, { recursive: true });
148
+ writeFileSync(join(stateDir, KXM_UPDATE_CACHE), `${JSON.stringify({ checkedAt: now, notice })}\n`, { encoding: "utf8" });
149
+ }
150
+
151
+ function githubReleaseAsset(assets: unknown, version: string): KxmReleaseAsset | undefined {
152
+ if (!Array.isArray(assets)) return undefined;
153
+ const name = kxmReleaseAssetName(version);
154
+ const match = assets.find((item) => item && typeof item === "object" && (item as { name?: unknown }).name === name);
155
+ if (!match || typeof match !== "object") return undefined;
156
+ const digest = (match as { digest?: unknown }).digest;
157
+ if (typeof digest !== "string") return undefined;
158
+ const parsed = SHA256_DIGEST.exec(digest.trim());
159
+ if (!parsed) return undefined;
160
+ return { name, sha256: parsed[1]!.toLowerCase() };
161
+ }
162
+
163
+ export async function fetchLatestKxmVersion(
164
+ source: KxmUpdateSource,
165
+ env: NodeJS.ProcessEnv,
166
+ fetchImpl: typeof fetch = fetch,
167
+ ): Promise<{ latest?: string; error?: string; asset?: KxmReleaseAsset }> {
168
+ const controller = new AbortController();
169
+ const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
170
+ try {
171
+ if (source === "npm") {
172
+ const response = await fetchImpl("https://registry.npmjs.org/@kontextmind/kxm/latest", { signal: controller.signal });
173
+ if (!response.ok) return { error: `npm_http_${response.status}` };
174
+ const body = await response.json() as { version?: unknown };
175
+ if (typeof body.version !== "string" || !parseSemver(body.version)) return { error: "npm_version_invalid" };
176
+ return { latest: body.version };
177
+ }
178
+ const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
179
+ const token = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim();
180
+ if (token) headers.Authorization = `Bearer ${token}`;
181
+ const response = await fetchImpl("https://api.github.com/repos/kontextmind/kxm/releases/latest", {
182
+ signal: controller.signal,
183
+ headers,
184
+ });
185
+ if (!response.ok) return { error: `github_http_${response.status}` };
186
+ const body = await response.json() as { tag_name?: unknown; assets?: unknown };
187
+ if (typeof body.tag_name !== "string") return { error: "github_tag_invalid" };
188
+ const version = body.tag_name.replace(/^v/, "");
189
+ if (!parseSemver(version)) return { error: "github_version_invalid" };
190
+ const asset = githubReleaseAsset(body.assets, version);
191
+ return asset ? { latest: version, asset } : { latest: version };
192
+ } catch (error) {
193
+ const aborted = error instanceof Error && error.name === "AbortError";
194
+ return { error: aborted ? "timeout" : "unreachable" };
195
+ } finally {
196
+ clearTimeout(timer);
197
+ }
198
+ }
199
+
200
+ export type KxmPackageUpdateStep =
201
+ | { kind: "download"; command: "gh"; args: string[] }
202
+ | { kind: "verify"; path: string; sha256: string }
203
+ | { kind: "install"; command: "npm"; args: string[] };
204
+
205
+ export function verifyReleaseAssetDigest(path: string, sha256: string): boolean {
206
+ if (!existsSync(path)) return false;
207
+ try {
208
+ const actual = createHash("sha256").update(readFileSync(path)).digest("hex");
209
+ return actual === sha256.toLowerCase();
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ /** npm registry apply is for after the public package exists. npm verifies registry integrity itself; do not duplicate that check here. Git installs use GitHub release tarballs. */
216
+ export function planKxmPackageUpdate(
217
+ source: KxmUpdateSource,
218
+ latest: string,
219
+ releaseDir: string,
220
+ asset?: KxmReleaseAsset,
221
+ ): KxmPackageUpdateStep[] {
222
+ if (source === "npm") {
223
+ return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
224
+ }
225
+ const name = asset?.name ?? kxmReleaseAssetName(latest);
226
+ const steps: KxmPackageUpdateStep[] = [
227
+ {
228
+ kind: "download",
229
+ command: "gh",
230
+ args: ["release", "download", `v${latest}`, "--repo", "kontextmind/kxm", "--pattern", name, "--dir", releaseDir, "--clobber"],
231
+ },
232
+ ];
233
+ if (asset?.sha256) {
234
+ steps.push({ kind: "verify", path: join(releaseDir, name), sha256: asset.sha256 });
235
+ }
236
+ steps.push({ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", join(releaseDir, name)] });
237
+ return steps;
238
+ }
@@ -0,0 +1,406 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import type { AgentRecord, MessageRecord } from "./protocol.ts";
6
+ import type { WorkflowRun } from "./workflow.ts";
7
+ import { readRoutingRecords } from "./telemetry.ts";
8
+ import type { RoutingRecord, RoutingRecordV2 } from "./routing.ts";
9
+
10
+ export interface MeshTuiPidClaim {
11
+ file: string;
12
+ role?: string;
13
+ pid?: number;
14
+ live: boolean;
15
+ }
16
+
17
+ export interface MeshTuiRunStage {
18
+ id: string;
19
+ label?: string;
20
+ status: string;
21
+ attempts?: number;
22
+ }
23
+
24
+ export interface MeshTuiRun {
25
+ id: string;
26
+ status: string;
27
+ definitionId: string;
28
+ project: string;
29
+ currentStage?: string;
30
+ targetAgentName?: string;
31
+ updatedAt?: string;
32
+ progress?: { done: number; total: number };
33
+ stages?: MeshTuiRunStage[];
34
+ }
35
+
36
+ export interface MeshTuiPlan {
37
+ id: string;
38
+ runId: string;
39
+ summary: string;
40
+ createdAt: string;
41
+ stageId?: string;
42
+ severity?: string;
43
+ }
44
+
45
+ export type MeshTuiOpenMessage = Pick<MessageRecord, "id" | "status" | "fromName" | "toName" | "delivery" | "createdAt" | "correlationId">;
46
+
47
+ export interface LocalMeshSnapshot {
48
+ source?: "legacy" | "vnext" | "both" | undefined;
49
+ agents: AgentRecord[];
50
+ openMessages: MeshTuiOpenMessage[];
51
+ openMessageTotal: number;
52
+ runs: MeshTuiRun[];
53
+ runTotal: number;
54
+ plans: MeshTuiPlan[];
55
+ pids: MeshTuiPidClaim[];
56
+ spend?: Array<{ recordedAt: string; routing: RoutingRecord | RoutingRecordV2 }> | undefined;
57
+ }
58
+
59
+ function processExists(pid: number): boolean {
60
+ try {
61
+ process.kill(pid, 0);
62
+ return true;
63
+ } catch (error) {
64
+ return (error as NodeJS.ErrnoException).code === "EPERM";
65
+ }
66
+ }
67
+
68
+ function readJsonRows<T>(database: DatabaseSync, sql: string): T[] {
69
+ const rows = database.prepare(sql).all() as Array<{ record: string }>;
70
+ const out: T[] = [];
71
+ for (const row of rows) {
72
+ try {
73
+ out.push(JSON.parse(row.record) as T);
74
+ } catch {
75
+ // skip corrupt rows
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+
81
+ const DONE_STAGE_STATUSES = new Set(["passed", "failed", "warning"]);
82
+
83
+ export function summarizeMeshRun(run: Pick<WorkflowRun, "id" | "status" | "definitionId" | "project" | "currentStage" | "targetAgentName" | "updatedAt" | "stages">): MeshTuiRun {
84
+ const stages = (run.stages ?? []).map((stage) => ({
85
+ id: stage.id,
86
+ ...(stage.label ? { label: stage.label } : {}),
87
+ status: stage.status,
88
+ ...(stage.attempts ? { attempts: stage.attempts } : {}),
89
+ }));
90
+ const total = stages.length;
91
+ const done = stages.filter((stage) => DONE_STAGE_STATUSES.has(stage.status)).length;
92
+ return {
93
+ id: run.id,
94
+ status: run.status,
95
+ definitionId: run.definitionId,
96
+ project: run.project,
97
+ ...(run.currentStage ? { currentStage: run.currentStage } : {}),
98
+ ...(run.targetAgentName ? { targetAgentName: run.targetAgentName } : {}),
99
+ ...(run.updatedAt ? { updatedAt: run.updatedAt } : {}),
100
+ ...(total > 0 ? { progress: { done, total }, stages } : {}),
101
+ };
102
+ }
103
+
104
+ function summarizeMeshPlan(entry: { id: string; runId: string; category?: string; summary: string; createdAt: string; stageId?: string; severity?: string }): MeshTuiPlan | undefined {
105
+ if (entry.category && entry.category !== "plan") return undefined;
106
+ const summary = entry.summary.replace(/\s+/g, " ").trim().slice(0, 120);
107
+ if (!summary) return undefined;
108
+ return {
109
+ id: entry.id,
110
+ runId: entry.runId,
111
+ summary,
112
+ createdAt: entry.createdAt,
113
+ ...(entry.stageId ? { stageId: entry.stageId } : {}),
114
+ ...(entry.severity ? { severity: entry.severity } : {}),
115
+ };
116
+ }
117
+
118
+ function readPlanMetadata(database: DatabaseSync): MeshTuiPlan[] {
119
+ try {
120
+ const rows = database.prepare(`
121
+ SELECT record FROM workflow_journal
122
+ WHERE category = 'plan'
123
+ ORDER BY rowid DESC
124
+ LIMIT 16
125
+ `).all() as Array<{ record: string }>;
126
+ const plans: MeshTuiPlan[] = [];
127
+ for (const row of rows) {
128
+ try {
129
+ const parsed = JSON.parse(row.record) as {
130
+ id?: string;
131
+ runId?: string;
132
+ category?: string;
133
+ summary?: string;
134
+ createdAt?: string;
135
+ stageId?: string;
136
+ severity?: string;
137
+ details?: string;
138
+ };
139
+ if (typeof parsed.id !== "string" || typeof parsed.runId !== "string" || typeof parsed.summary !== "string" || typeof parsed.createdAt !== "string") continue;
140
+ const plan = summarizeMeshPlan({
141
+ id: parsed.id,
142
+ runId: parsed.runId,
143
+ summary: parsed.summary,
144
+ createdAt: parsed.createdAt,
145
+ ...(parsed.category ? { category: parsed.category } : {}),
146
+ ...(parsed.stageId ? { stageId: parsed.stageId } : {}),
147
+ ...(parsed.severity ? { severity: parsed.severity } : {}),
148
+ });
149
+ if (plan) plans.push(plan);
150
+ } catch { /* skip malformed journal rows */ }
151
+ }
152
+ return plans;
153
+ } catch {
154
+ return [];
155
+ }
156
+ }
157
+
158
+ function countRows(database: DatabaseSync, table: "messages" | "workflow_runs", where = ""): number {
159
+ const row = database.prepare(`SELECT COUNT(*) AS count FROM ${table}${where}`).get() as { count?: number | bigint } | undefined;
160
+ return Number(row?.count ?? 0);
161
+ }
162
+
163
+ function readOpenMessageMetadata(database: DatabaseSync): MeshTuiOpenMessage[] {
164
+ const rows = database.prepare(`
165
+ SELECT
166
+ json_extract(record, '$.id') AS id,
167
+ json_extract(record, '$.status') AS status,
168
+ COALESCE(json_extract(record, '$.fromName'), json_extract(record, '$.from')) AS fromName,
169
+ COALESCE(json_extract(record, '$.toName'), json_extract(record, '$.to')) AS toName,
170
+ json_extract(record, '$.delivery') AS delivery,
171
+ json_extract(record, '$.createdAt') AS createdAt,
172
+ json_extract(record, '$.correlationId') AS correlationId
173
+ FROM messages
174
+ WHERE json_extract(record, '$.status') IN ('queued', 'delivered')
175
+ ORDER BY json_extract(record, '$.createdAt') DESC
176
+ LIMIT 16
177
+ `).all() as Array<Record<string, unknown>>;
178
+ const messages: MeshTuiOpenMessage[] = [];
179
+ for (const row of rows) {
180
+ if (
181
+ typeof row.id !== "string"
182
+ || (row.status !== "queued" && row.status !== "delivered")
183
+ || typeof row.fromName !== "string"
184
+ || typeof row.toName !== "string"
185
+ || (row.delivery !== "steer" && row.delivery !== "followUp" && row.delivery !== "nextTurn")
186
+ || typeof row.createdAt !== "string"
187
+ ) continue;
188
+ messages.push({
189
+ id: row.id,
190
+ status: row.status,
191
+ fromName: row.fromName,
192
+ toName: row.toName,
193
+ delivery: row.delivery,
194
+ createdAt: row.createdAt,
195
+ ...(typeof row.correlationId === "string" ? { correlationId: row.correlationId } : {}),
196
+ });
197
+ }
198
+ return messages;
199
+ }
200
+
201
+ export function resolveKxmSnapshotPaths(cwd: string, env: NodeJS.ProcessEnv = process.env): { dataPath: string; stateDir: string } {
202
+ const stateDir = env.KXM_STATE_DIR?.trim() || join(cwd, ".kxm", "state");
203
+ const configured = env.KXM_DATA_PATH?.trim();
204
+ const dataPath = configured ? resolve(cwd, configured) : join(stateDir, "kxm.db");
205
+ return { dataPath, stateDir };
206
+ }
207
+
208
+ function resolveVnextStateRoot(
209
+ stateDir: string,
210
+ options?: { env?: NodeJS.ProcessEnv; vnextStateRoot?: string },
211
+ ): string | undefined {
212
+ if (options?.vnextStateRoot && existsSync(options.vnextStateRoot)) {
213
+ return resolve(options.vnextStateRoot);
214
+ }
215
+ if (existsSync(join(stateDir, "runtime", "registry.db")) || existsSync(join(stateDir, "runtime", "projects"))) {
216
+ return stateDir;
217
+ }
218
+ const env = options?.env ?? process.env;
219
+ const explicit = env.KXM_STATE_HOME?.trim() || env.KXM_USER_STATE_DIR?.trim() || env.KXM_STATE_ROOT?.trim();
220
+ if (explicit && isAbsolute(explicit) && existsSync(resolve(explicit))) {
221
+ return resolve(explicit);
222
+ }
223
+ let base: string;
224
+ if (process.platform === "win32") {
225
+ const localAppData = env.LOCALAPPDATA?.trim();
226
+ base = localAppData && isAbsolute(localAppData) ? localAppData : join(homedir(), "AppData", "Local");
227
+ base = resolve(base, "KXM");
228
+ } else if (process.platform === "darwin") {
229
+ base = resolve(homedir(), "Library", "Application Support", "KXM");
230
+ } else {
231
+ const xdgState = env.XDG_STATE_HOME?.trim();
232
+ base = xdgState && isAbsolute(xdgState) ? xdgState : join(homedir(), ".local", "state");
233
+ base = resolve(base, "kxm");
234
+ }
235
+ if (existsSync(base)) return base;
236
+ return undefined;
237
+ }
238
+
239
+ export function loadLocalMeshSnapshot(
240
+ dataPath: string,
241
+ stateDir: string,
242
+ options?: { env?: NodeJS.ProcessEnv; projectRoot?: string; vnextStateRoot?: string },
243
+ ): LocalMeshSnapshot {
244
+ let hasLegacy = false;
245
+ let agents: AgentRecord[] = [];
246
+ let openMessages: MeshTuiOpenMessage[] = [];
247
+ let openMessageTotal = 0;
248
+ let legacyRuns: WorkflowRun[] = [];
249
+ let legacyRunTotal = 0;
250
+ let plans: MeshTuiPlan[] = [];
251
+ if (existsSync(dataPath)) {
252
+ hasLegacy = true;
253
+ const database = new DatabaseSync(dataPath, { readOnly: true });
254
+ try {
255
+ database.exec("PRAGMA busy_timeout = 5000");
256
+ agents = readJsonRows<AgentRecord>(database, "SELECT record FROM agents");
257
+ openMessages = readOpenMessageMetadata(database);
258
+ openMessageTotal = countRows(database, "messages", " WHERE json_extract(record, '$.status') IN ('queued', 'delivered')");
259
+ legacyRuns = readJsonRows<WorkflowRun>(database, "SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 8");
260
+ legacyRunTotal = countRows(database, "workflow_runs");
261
+ plans = readPlanMetadata(database);
262
+ } finally {
263
+ database.close();
264
+ }
265
+ }
266
+
267
+ // vNext discovery
268
+ let hasVnext = false;
269
+ const vnextRuns: MeshTuiRun[] = [];
270
+ let vnextRunTotal = 0;
271
+ const vnextStateRoot = resolveVnextStateRoot(stateDir, options);
272
+ if (vnextStateRoot) {
273
+ const runtimeDir = join(vnextStateRoot, "runtime");
274
+ const registryDbPath = join(runtimeDir, "registry.db");
275
+ const projectsDir = join(runtimeDir, "projects");
276
+
277
+ const projectKeys = new Set<string>();
278
+
279
+ if (existsSync(registryDbPath)) {
280
+ hasVnext = true;
281
+ try {
282
+ const regDb = new DatabaseSync(registryDbPath, { readOnly: true });
283
+ try {
284
+ regDb.exec("PRAGMA busy_timeout = 5000");
285
+ const pRows = regDb.prepare("SELECT project_key FROM projects").all() as Array<{ project_key: string }>;
286
+ for (const row of pRows) {
287
+ if (row.project_key) projectKeys.add(row.project_key);
288
+ }
289
+ } finally {
290
+ regDb.close();
291
+ }
292
+ } catch { /* registry error */ }
293
+ }
294
+
295
+ if (existsSync(projectsDir)) {
296
+ try {
297
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
298
+ if (entry.isDirectory()) {
299
+ projectKeys.add(entry.name);
300
+ }
301
+ }
302
+ } catch { /* projectsDir unreadable */ }
303
+ }
304
+
305
+ for (const key of projectKeys) {
306
+ const eventDbPath = join(projectsDir, key, "run-events.db");
307
+ if (existsSync(eventDbPath)) {
308
+ hasVnext = true;
309
+ try {
310
+ const eventDb = new DatabaseSync(eventDbPath, { readOnly: true });
311
+ try {
312
+ eventDb.exec("PRAGMA busy_timeout = 5000");
313
+ const runRows = eventDb.prepare(`
314
+ SELECT run_id, project_id, workflow_id, status, created_at, updated_at
315
+ FROM runs ORDER BY created_at DESC, run_id DESC LIMIT 8
316
+ `).all() as Array<{
317
+ run_id: string;
318
+ project_id: string;
319
+ workflow_id: string;
320
+ status: string;
321
+ created_at: string;
322
+ updated_at: string;
323
+ }>;
324
+ const countRow = eventDb.prepare("SELECT COUNT(*) AS total FROM runs").get() as { total: number } | undefined;
325
+ vnextRunTotal += Number(countRow?.total ?? runRows.length);
326
+ for (const r of runRows) {
327
+ vnextRuns.push({
328
+ id: r.run_id,
329
+ status: r.status,
330
+ definitionId: r.workflow_id,
331
+ project: r.project_id,
332
+ updatedAt: r.updated_at || r.created_at,
333
+ });
334
+ }
335
+ } finally {
336
+ eventDb.close();
337
+ }
338
+ } catch { /* eventDb unreadable */ }
339
+ }
340
+ }
341
+ }
342
+
343
+ const pids: MeshTuiPidClaim[] = [];
344
+ if (existsSync(stateDir)) {
345
+ for (const file of readdirSync(stateDir).filter((name) => name.endsWith(".pid"))) {
346
+ try {
347
+ const record = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as { pid?: number; role?: string };
348
+ pids.push({
349
+ file,
350
+ ...(record.role ? { role: record.role } : {}),
351
+ ...(record.pid !== undefined ? { pid: record.pid } : {}),
352
+ live: Number.isInteger(record.pid) && record.pid! > 0 && processExists(record.pid!),
353
+ });
354
+ } catch {
355
+ pids.push({ file, live: false });
356
+ }
357
+ }
358
+ }
359
+
360
+ // Combine runs
361
+ const combinedRuns = [
362
+ ...legacyRuns.map((run) => summarizeMeshRun(run)),
363
+ ...vnextRuns,
364
+ ];
365
+ const seenIds = new Set<string>();
366
+ const uniqueRuns: MeshTuiRun[] = [];
367
+ for (const run of combinedRuns) {
368
+ if (!seenIds.has(run.id)) {
369
+ seenIds.add(run.id);
370
+ uniqueRuns.push(run);
371
+ }
372
+ }
373
+ uniqueRuns.sort((a, b) => {
374
+ const at = a.updatedAt ? Date.parse(a.updatedAt) : 0;
375
+ const bt = b.updatedAt ? Date.parse(b.updatedAt) : 0;
376
+ return bt - at;
377
+ });
378
+ const runs = uniqueRuns.slice(0, 16);
379
+ const runTotal = legacyRunTotal + vnextRunTotal;
380
+
381
+ // Source determination
382
+ let source: "legacy" | "vnext" | "both";
383
+ if (hasLegacy && hasVnext) {
384
+ source = "both";
385
+ } else if (hasVnext) {
386
+ source = "vnext";
387
+ } else {
388
+ source = "legacy";
389
+ }
390
+
391
+ // Spend from telemetry
392
+ const telemetryFile = join(stateDir, "telemetry.jsonl");
393
+ const spend = existsSync(telemetryFile) ? readRoutingRecords(telemetryFile) : [];
394
+
395
+ return {
396
+ source,
397
+ agents: agents.sort((a, b) => Number(b.online) - Number(a.online) || a.name.localeCompare(b.name)),
398
+ openMessages,
399
+ openMessageTotal,
400
+ runs,
401
+ runTotal,
402
+ plans,
403
+ pids,
404
+ spend,
405
+ };
406
+ }