@kontextmind/kxm 0.7.178 → 0.7.180

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.
@@ -37827,12 +37827,12 @@ var MAX_SESSION_BRIEF_TASKS = 5;
37827
37827
  var MAX_SESSION_BRIEF_PLANS = 5;
37828
37828
  var SESSION_BRIEF_SCHEMA = "kxm.session-brief.v1";
37829
37829
  var DEFAULT_SESSION_BRIEF_STALE_SECONDS = 5;
37830
- function hubPrefix(hub) {
37830
+ function hubStatusPart(hub) {
37831
37831
  const suffix = hub?.scope === "remote" ? "/remote" : "";
37832
- if (hub?.state === "on" || hub?.state === void 0 && hub?.online === true) return `kxm hub:on${suffix}`;
37833
- if (hub?.state === "off" || hub?.state === void 0 && hub?.online === false) return `kxm hub:off${suffix}`;
37834
- if (hub?.state === "unknown") return `kxm hub:unknown${suffix}`;
37835
- return "kxm";
37832
+ if (hub?.state === "off" || hub?.state === void 0 && hub?.online === false) return `hub:off${suffix}`;
37833
+ if (hub?.state === "unknown") return `hub:unknown${suffix}`;
37834
+ if (hub?.scope === "remote") return "hub:remote";
37835
+ return void 0;
37836
37836
  }
37837
37837
  function truncate(value, width) {
37838
37838
  const text = value.replace(/\s+/g, " ").trim();
@@ -37908,40 +37908,34 @@ function readGitShip(cwd) {
37908
37908
  return void 0;
37909
37909
  }
37910
37910
  }
37911
- function formatSessionStatusLine(stats, current, hub, ship, updateLatest, cost) {
37912
- const head = hubPrefix(hub);
37913
- if (!current && stats.activeTasks === 0 && stats.planCount === 0 && stats.inbox === 0) {
37914
- const idle = hub?.online === void 0 && hub?.state === void 0 ? "kxm idle" : `${head} \xB7 idle`;
37915
- const withShip = ship?.dirty ? `${idle} \xB7 dirty` : ship && ship.ahead > 0 ? `${idle} \xB7 ${ship.ahead} local` : idle;
37916
- const withCost = cost ? `${withShip} \xB7 ${cost}` : withShip;
37917
- const finalLine = updateLatest ? `${withCost} \xB7 upd ${updateLatest}` : withCost;
37918
- return finalLine.length <= 80 ? finalLine : `${finalLine.slice(0, 79)}\u2026`;
37919
- }
37920
- const parts = [];
37921
- if (current?.kind === "task") parts.push(`${head} ${current.detail}`);
37922
- else if (current?.kind === "plan") parts.push(`${head} plan ${truncate(current.detail, 36)}`);
37923
- else parts.push(head);
37924
- parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
37911
+ function formatSessionStatusLine(stats, current, hub, ship, updateLatest) {
37912
+ const parts = ["kxm"];
37913
+ const hubPart = hubStatusPart(hub);
37914
+ if (hubPart) parts.push(hubPart);
37915
+ if (current?.kind === "task") parts.push(current.detail);
37916
+ else if (current?.kind === "plan") parts.push(`plan ${truncate(current.detail, 36)}`);
37917
+ if (stats.activeTasks > 0) parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
37925
37918
  if (stats.waitingTasks > 0) parts.push(`${stats.waitingTasks} waiting`);
37926
- parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
37919
+ if (stats.planCount > 0) parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
37927
37920
  if (stats.inbox > 0) parts.push(`inbox ${stats.inbox}`);
37928
37921
  if (ship?.dirty) parts.push("dirty");
37929
37922
  else if (ship && ship.ahead > 0) parts.push(`${ship.ahead} local`);
37930
- if (cost) parts.push(cost);
37931
37923
  if (updateLatest) parts.push(`upd ${updateLatest}`);
37932
- const line = parts.join(" \xB7 ");
37933
- return line.length <= 80 ? line : `${line.slice(0, 79)}\u2026`;
37924
+ return truncate(parts.join(" \xB7 "), 80);
37934
37925
  }
37935
37926
  function formatSessionWidget(stats, current, hub, ship, updateLatest, cost) {
37936
- const hubMark = hub?.state === "on" || hub?.online === true ? "hub:on " : hub?.state === "off" || hub?.online === false ? "hub:off " : hub?.state === "unknown" ? "hub:unknown " : "";
37937
- const lines = [
37938
- `KXM ${hubMark}${stats.activeTasks} tasks ${stats.waitingTasks} waiting ${stats.planCount} plans inbox ${stats.inbox}`
37939
- ];
37927
+ const suffix = hub?.scope === "remote" ? "/remote" : "";
37928
+ const hubMark = hub?.state === "on" || hub?.state === void 0 && hub?.online === true ? `hub:on${suffix}` : hub?.state === "off" || hub?.state === void 0 && hub?.online === false ? `hub:off${suffix}` : hub?.state === "unknown" ? `hub:unknown${suffix}` : void 0;
37929
+ const summary = ["KXM", hubMark];
37930
+ if (stats.activeTasks > 0) summary.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
37931
+ if (stats.waitingTasks > 0) summary.push(`${stats.waitingTasks} waiting`);
37932
+ if (stats.planCount > 0) summary.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
37933
+ if (stats.inbox > 0) summary.push(`inbox ${stats.inbox}`);
37934
+ const lines = [summary.filter(Boolean).join(" \xB7 ")];
37940
37935
  if (current) lines.push(`now ${current.kind} ${truncate(current.detail, 56)}`);
37941
- else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
37942
- else lines.push("now no selected work");
37943
- lines.push(formatShipLine(ship));
37944
- if (cost) lines.push(`cost ${cost}`);
37936
+ else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
37937
+ if (ship?.dirty || ship && ship.ahead > 0) lines.push(formatShipLine(ship));
37938
+ if (cost && cost !== "unknown") lines.push(`cost ${cost}`);
37945
37939
  if (updateLatest) lines.push(`update ${updateLatest} available \xB7 kxm update --kxm`);
37946
37940
  return lines;
37947
37941
  }
@@ -37961,7 +37955,7 @@ function buildSessionBrief(snapshot, current, hub, ship, updateLatest, cost, ses
37961
37955
  const tasks = uniqueLabels(recentTasks(snapshot.runs).map(taskItem));
37962
37956
  const plans = uniqueLabels(snapshot.plans.slice(0, MAX_SESSION_BRIEF_PLANS).map(planItem));
37963
37957
  const resolvedHub = hub ?? { state: "off", evidence: "unconfigured", online: false };
37964
- const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest, cost);
37958
+ const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest);
37965
37959
  const widgetLines = formatSessionWidget(stats, current, resolvedHub, ship, updateLatest, cost);
37966
37960
  return {
37967
37961
  schema: SESSION_BRIEF_SCHEMA,
@@ -38025,7 +38019,7 @@ function estimateSessionCost(stateDir) {
38025
38019
  latestModel = r.effectiveModel ?? r.requestedModel ?? latestModel;
38026
38020
  latestHarness = r.harness ?? latestHarness;
38027
38021
  }
38028
- if (!hasMetered) return "unknown";
38022
+ if (!hasMetered) return void 0;
38029
38023
  const route = latestHarness && latestModel ? `${latestHarness}/${latestModel}` : latestModel;
38030
38024
  return `$${totalCost.toFixed(2)} sess${route ? ` \xB7 ${route}` : ""}`;
38031
38025
  } catch {
@@ -38062,8 +38056,7 @@ function loadSessionBrief(cwd, env = process.env, current, hub, options = {}) {
38062
38056
  current,
38063
38057
  effectiveHub,
38064
38058
  options.ship,
38065
- options.updateLatest,
38066
- effectiveCost
38059
+ options.updateLatest
38067
38060
  );
38068
38061
  const widgetLines = formatSessionWidget(
38069
38062
  cached2.stats,
@@ -38128,8 +38121,7 @@ async function loadSessionBriefAsync(cwd, env = process.env, current, hub, optio
38128
38121
  current,
38129
38122
  effectiveHub,
38130
38123
  options.ship,
38131
- void 0,
38132
- effectiveCost
38124
+ void 0
38133
38125
  );
38134
38126
  const widgetLines = formatSessionWidget(
38135
38127
  cached2.stats,
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
25462
25462
  }
25463
25463
 
25464
25464
  // plugins/kxm/src/mcp-server.ts
25465
- var VERSION = "0.7.178";
25465
+ var VERSION = "0.7.180";
25466
25466
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25467
25467
  var inbox = /* @__PURE__ */ new Map();
25468
25468
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.178",
3
+ "version": "0.7.180",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,7 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
2
5
  import { findKxmRepoRoot } from "../repo-root.ts";
3
6
  import { probeHarnesses } from "../harness.ts";
4
- import { print, type Runtime } from "./types.ts";
7
+ import { print, type CliSpawnResult, type Runtime } from "./types.ts";
5
8
 
6
9
  export type PluginHarnessId = "pi" | "omp" | "claude";
7
10
 
@@ -12,6 +15,7 @@ export interface PluginInstallOptions {
12
15
  claude?: boolean | undefined;
13
16
  omp?: boolean | undefined;
14
17
  pi?: boolean | undefined;
18
+ statusBand?: boolean | undefined;
15
19
  }
16
20
 
17
21
  export interface PluginInstallHarnessResult {
@@ -23,6 +27,149 @@ export interface PluginInstallHarnessResult {
23
27
  error?: string | undefined;
24
28
  }
25
29
 
30
+ const OMP_STATUS_SETTINGS = [
31
+ { key: "statusLine.leftSegments", configKey: "leftSegments", value: ["model", "mode", "git", "pr", "subagents"] },
32
+ { key: "statusLine.rightSegments", configKey: "rightSegments", value: ["usage", "cost", "context_pct"] },
33
+ { key: "statusLine.preset", configKey: "preset", value: "custom" },
34
+ ] as const;
35
+
36
+ function spawnHarness(runtime: Runtime, command: string, args: readonly string[]): CliSpawnResult {
37
+ return runtime.io.spawnSync
38
+ ? runtime.io.spawnSync(command, args)
39
+ : spawnSync(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
40
+ }
41
+
42
+ function spawnFailed(run: CliSpawnResult): boolean {
43
+ return Boolean(run.error) || run.status !== 0;
44
+ }
45
+
46
+ function spawnFailureDetail(run: CliSpawnResult, fallback: string): string {
47
+ return (run.stderr || run.stdout || run.error?.message || fallback).trim();
48
+ }
49
+
50
+ interface OmpStatusBandFailure {
51
+ args: readonly string[];
52
+ run: CliSpawnResult;
53
+ rollbackAttempted: boolean;
54
+ rollbackFailed: boolean;
55
+ }
56
+
57
+ interface OmpGlobalSettingSnapshot {
58
+ present: boolean;
59
+ value?: unknown;
60
+ }
61
+
62
+ function ompConfigValueArg(value: unknown): string {
63
+ return typeof value === "string" ? value : JSON.stringify(value);
64
+ }
65
+
66
+ function readOmpGlobalStatusSettings(
67
+ runtime: Runtime,
68
+ command: string,
69
+ ): { previous: Record<string, OmpGlobalSettingSnapshot> } | OmpStatusBandFailure {
70
+ const args = ["config", "path"];
71
+ const run = spawnHarness(runtime, command, args);
72
+ if (spawnFailed(run)) return { args, run, rollbackAttempted: false, rollbackFailed: false };
73
+
74
+ const agentDir = run.stdout.trim();
75
+ if (!agentDir || !isAbsolute(agentDir) || !existsSync(agentDir)) {
76
+ return {
77
+ args,
78
+ run: { status: 1, stdout: run.stdout, stderr: "omp config path did not return an existing absolute directory" },
79
+ rollbackAttempted: false,
80
+ rollbackFailed: false,
81
+ };
82
+ }
83
+
84
+ const configPath = ["config.yml", "config.yaml"].map((name) => join(agentDir, name)).find(existsSync);
85
+ let document: unknown = {};
86
+ try {
87
+ if (configPath) document = parseYaml(readFileSync(configPath, "utf8"));
88
+ } catch {
89
+ return {
90
+ args,
91
+ run: { status: 1, stdout: run.stdout, stderr: `could not read OMP global config at ${configPath}` },
92
+ rollbackAttempted: false,
93
+ rollbackFailed: false,
94
+ };
95
+ }
96
+
97
+ const root = document && typeof document === "object" && !Array.isArray(document) ? document : {};
98
+ const rawStatusLine = "statusLine" in root ? root.statusLine : undefined;
99
+ const statusLine = rawStatusLine && typeof rawStatusLine === "object" && !Array.isArray(rawStatusLine)
100
+ ? rawStatusLine
101
+ : {};
102
+ const previous: Record<string, OmpGlobalSettingSnapshot> = {};
103
+ for (const setting of OMP_STATUS_SETTINGS) {
104
+ const value = statusLine[setting.configKey as keyof typeof statusLine];
105
+ previous[setting.key] = Object.hasOwn(statusLine, setting.configKey) && value !== null && value !== undefined
106
+ ? { present: true, value }
107
+ : { present: false };
108
+ }
109
+ return { previous };
110
+ }
111
+
112
+ function rollbackOmpStatusBand(
113
+ runtime: Runtime,
114
+ command: string,
115
+ settings: readonly (typeof OMP_STATUS_SETTINGS)[number][],
116
+ previous: Readonly<Record<string, OmpGlobalSettingSnapshot>>,
117
+ ): boolean {
118
+ let failed = false;
119
+ for (const setting of [...settings].reverse()) {
120
+ const snapshot = previous[setting.key];
121
+ const args = snapshot?.present
122
+ ? ["config", "set", setting.key, ompConfigValueArg(snapshot.value), "--json"]
123
+ : ["config", "reset", setting.key, "--json"];
124
+ if (spawnFailed(spawnHarness(runtime, command, args))) failed = true;
125
+ }
126
+ return failed;
127
+ }
128
+
129
+ function configureOmpStatusBand(runtime: Runtime, command: string): OmpStatusBandFailure | undefined {
130
+ const snapshot = readOmpGlobalStatusSettings(runtime, command);
131
+ if ("run" in snapshot) return snapshot;
132
+
133
+ const written: (typeof OMP_STATUS_SETTINGS)[number][] = [];
134
+ for (const setting of OMP_STATUS_SETTINGS) {
135
+ const args = ["config", "set", setting.key, ompConfigValueArg(setting.value), "--json"];
136
+ written.push(setting);
137
+ const run = spawnHarness(runtime, command, args);
138
+ if (spawnFailed(run)) {
139
+ return {
140
+ args,
141
+ run,
142
+ rollbackAttempted: true,
143
+ rollbackFailed: rollbackOmpStatusBand(runtime, command, written, snapshot.previous),
144
+ };
145
+ }
146
+ }
147
+
148
+ for (const setting of OMP_STATUS_SETTINGS) {
149
+ const args = ["config", "get", setting.key, "--json"];
150
+ const run = spawnHarness(runtime, command, args);
151
+ let actual: unknown;
152
+ try {
153
+ const parsed: unknown = JSON.parse(run.stdout);
154
+ actual = parsed && typeof parsed === "object" && "value" in parsed ? parsed.value : undefined;
155
+ } catch {
156
+ actual = undefined;
157
+ }
158
+ if (spawnFailed(run) || JSON.stringify(actual) !== JSON.stringify(setting.value)) {
159
+ const mismatch = spawnFailed(run)
160
+ ? run
161
+ : { status: 1, stdout: run.stdout, stderr: `effective ${setting.key} is shadowed or did not persist` };
162
+ return {
163
+ args,
164
+ run: mismatch,
165
+ rollbackAttempted: true,
166
+ rollbackFailed: rollbackOmpStatusBand(runtime, command, written, snapshot.previous),
167
+ };
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+
26
173
  export async function cmdPluginInstall(
27
174
  runtime: Runtime,
28
175
  options: PluginInstallOptions,
@@ -41,6 +188,17 @@ export async function cmdPluginInstall(
41
188
  ? requestedHarnesses
42
189
  : PLUGIN_SUPPORTED_HARNESSES;
43
190
 
191
+ if (options.statusBand && isExplicit && !options.omp) {
192
+ const detail = "--status-band requires OMP to be among the selected harnesses";
193
+ print(
194
+ runtime.io,
195
+ runtime.json,
196
+ { ok: false, command: "plugin install", error: "omp_status_band_requires_omp", results: [] },
197
+ detail,
198
+ );
199
+ return 1;
200
+ }
201
+
44
202
  const results: PluginInstallHarnessResult[] = [];
45
203
 
46
204
  for (const harnessId of targetHarnesses) {
@@ -48,7 +206,7 @@ export async function cmdPluginInstall(
48
206
  const detected = Boolean(status?.detected && status?.command);
49
207
 
50
208
  if (!detected) {
51
- if (isExplicit) {
209
+ if (isExplicit || (options.statusBand && harnessId === "omp")) {
52
210
  results.push({
53
211
  harness: harnessId,
54
212
  status: "failed",
@@ -72,17 +230,15 @@ export async function cmdPluginInstall(
72
230
  detail: `would install kxm plugin into pi: ${command} ${args.join(" ")}`,
73
231
  });
74
232
  } else {
75
- const run = runtime.io.spawnSync
76
- ? runtime.io.spawnSync(command, args)
77
- : spawnSync(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
78
- if (run.error || (run.status !== 0 && run.status !== null)) {
233
+ const run = spawnHarness(runtime, command, args);
234
+ if (spawnFailed(run)) {
79
235
  results.push({
80
236
  harness: "pi",
81
237
  status: "failed",
82
238
  command,
83
239
  args,
84
240
  error: run.error?.message || "install_failed",
85
- detail: (run.stderr || run.stdout || "pi install exited with error").trim(),
241
+ detail: spawnFailureDetail(run, "pi install exited with error"),
86
242
  });
87
243
  } else {
88
244
  results.push({
@@ -102,29 +258,43 @@ export async function cmdPluginInstall(
102
258
  status: "would",
103
259
  command,
104
260
  args,
105
- detail: `would install kxm plugin into omp: ${command} ${args.join(" ")}`,
261
+ detail: options.statusBand
262
+ ? `would install kxm plugin into omp and configure the focused subscription status band: ${command} ${args.join(" ")}`
263
+ : `would install kxm plugin into omp: ${command} ${args.join(" ")}`,
106
264
  });
107
265
  } else {
108
- const run = runtime.io.spawnSync
109
- ? runtime.io.spawnSync(command, args)
110
- : spawnSync(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
111
- if (run.error || (run.status !== 0 && run.status !== null)) {
266
+ const run = spawnHarness(runtime, command, args);
267
+ if (spawnFailed(run)) {
112
268
  results.push({
113
269
  harness: "omp",
114
270
  status: "failed",
115
271
  command,
116
272
  args,
117
273
  error: run.error?.message || "install_failed",
118
- detail: (run.stderr || run.stdout || "omp plugin install exited with error").trim(),
274
+ detail: spawnFailureDetail(run, "omp plugin install exited with error"),
119
275
  });
120
276
  } else {
121
- results.push({
122
- harness: "omp",
123
- status: "passed",
124
- command,
125
- args,
126
- detail: "installed kxm plugin into omp",
127
- });
277
+ const configFailure = options.statusBand ? configureOmpStatusBand(runtime, command) : undefined;
278
+ if (configFailure) {
279
+ results.push({
280
+ harness: "omp",
281
+ status: "failed",
282
+ command,
283
+ args: configFailure.args,
284
+ error: "status_band_config_failed",
285
+ detail: `installed kxm plugin into omp, but ${configFailure.args.slice(0, 3).join(" ")} failed: ${spawnFailureDetail(configFailure.run, "omp status band configuration exited with error")}${configFailure.rollbackAttempted ? (configFailure.rollbackFailed ? "; previous settings could not be fully restored" : "; previous settings restored") : ""}`,
286
+ });
287
+ } else {
288
+ results.push({
289
+ harness: "omp",
290
+ status: "passed",
291
+ command,
292
+ args,
293
+ detail: options.statusBand
294
+ ? "installed kxm plugin into omp and configured the focused subscription status band; restart omp"
295
+ : "installed kxm plugin into omp",
296
+ });
297
+ }
128
298
  }
129
299
  }
130
300
  } else if (harnessId === "claude") {
@@ -144,17 +314,15 @@ export async function cmdPluginInstall(
144
314
  } else {
145
315
  spawnSync(command, marketplaceArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
146
316
  }
147
- const run = runtime.io.spawnSync
148
- ? runtime.io.spawnSync(command, installArgs)
149
- : spawnSync(command, installArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
150
- if (run.error || (run.status !== 0 && run.status !== null)) {
317
+ const run = spawnHarness(runtime, command, installArgs);
318
+ if (spawnFailed(run)) {
151
319
  results.push({
152
320
  harness: "claude",
153
321
  status: "failed",
154
322
  command,
155
323
  args: installArgs,
156
324
  error: run.error?.message || "install_failed",
157
- detail: (run.stderr || run.stdout || "claude plugin install exited with error").trim(),
325
+ detail: spawnFailureDetail(run, "claude install exited with error"),
158
326
  });
159
327
  } else {
160
328
  results.push({
@@ -773,7 +773,8 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
773
773
  .option("--claude", "Install plugin for Claude Code")
774
774
  .option("--omp", "Install plugin for Oh My Pi (OMP)")
775
775
  .option("--pi", "Install plugin for Pi")
776
- .action(async function pluginInstallAction(this: Command, options: { all?: boolean; claude?: boolean; omp?: boolean; pi?: boolean }) {
776
+ .option("--status-band", "Configure OMP's focused status band with live subscription usage")
777
+ .action(async function pluginInstallAction(this: Command, options: { all?: boolean; claude?: boolean; omp?: boolean; pi?: boolean; statusBand?: boolean }) {
777
778
  result.code = await cmdPluginInstall(runtimeFrom(ctx, this), options);
778
779
  });
779
780
 
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.178";
14
+ const VERSION = "0.7.180";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -66,15 +66,12 @@ export interface SessionBrief {
66
66
  sessionToken?: string;
67
67
  }
68
68
 
69
- function hubPrefix(hub?: Partial<SessionHubStatus>): string {
70
- // A remote hub carries a bearer over a network; a loopback one does not. The JSON
71
- // said so while every text surface printed the same bytes for both, which is the
72
- // distinction an operator actually reads.
69
+ function hubStatusPart(hub?: Partial<SessionHubStatus>): string | undefined {
73
70
  const suffix = hub?.scope === "remote" ? "/remote" : "";
74
- if (hub?.state === "on" || (hub?.state === undefined && hub?.online === true)) return `kxm hub:on${suffix}`;
75
- if (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)) return `kxm hub:off${suffix}`;
76
- if (hub?.state === "unknown") return `kxm hub:unknown${suffix}`;
77
- return "kxm";
71
+ if (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)) return `hub:off${suffix}`;
72
+ if (hub?.state === "unknown") return `hub:unknown${suffix}`;
73
+ if (hub?.scope === "remote") return "hub:remote";
74
+ return undefined;
78
75
  }
79
76
 
80
77
  function truncate(value: string, width: number): string {
@@ -172,12 +169,12 @@ export function formatCostLine(input?: {
172
169
  model?: string;
173
170
  }): string | undefined {
174
171
  if (!input) return undefined;
175
- if (input.sessionCostUsd === undefined && input.runCostUsd === undefined) return "unknown";
172
+ if (input.sessionCostUsd === undefined && input.runCostUsd === undefined) return undefined;
176
173
  const sess = input.sessionCostUsd !== undefined ? `$${input.sessionCostUsd.toFixed(2)} sess` : undefined;
177
174
  const run = input.runCostUsd !== undefined ? `$${input.runCostUsd.toFixed(2)} run` : undefined;
178
175
  const route = input.harness && input.model ? `${input.harness}/${input.model}` : (input.model ?? undefined);
179
176
  const parts = [sess, run, route].filter(Boolean);
180
- return parts.length > 0 ? parts.join(" · ") : "unknown";
177
+ return parts.length > 0 ? parts.join(" · ") : undefined;
181
178
  }
182
179
 
183
180
  export function renderStatusLine(
@@ -186,11 +183,10 @@ export function renderStatusLine(
186
183
  hub?: Partial<SessionHubStatus>,
187
184
  ship?: SessionShipStatus,
188
185
  updateLatest?: string,
189
- cost?: string,
190
186
  ): string {
191
187
  if ("schema" in statsOrBrief && statsOrBrief.schema === "kxm.session-brief.v1") {
192
188
  const brief = statsOrBrief;
193
- if (!current && !hub && !ship && !updateLatest && !cost) {
189
+ if (!current && !hub && !ship && !updateLatest) {
194
190
  return brief.statusLine;
195
191
  }
196
192
  return formatSessionStatusLine(
@@ -199,10 +195,9 @@ export function renderStatusLine(
199
195
  hub ?? brief.hub,
200
196
  ship,
201
197
  updateLatest,
202
- cost ?? brief.cost,
203
198
  );
204
199
  }
205
- return formatSessionStatusLine(statsOrBrief as SessionWorkStats, current, hub, ship, updateLatest, cost);
200
+ return formatSessionStatusLine(statsOrBrief as SessionWorkStats, current, hub, ship, updateLatest);
206
201
  }
207
202
 
208
203
  export function formatSessionStatusLine(
@@ -211,32 +206,20 @@ export function formatSessionStatusLine(
211
206
  hub?: Partial<SessionHubStatus>,
212
207
  ship?: SessionShipStatus,
213
208
  updateLatest?: string,
214
- cost?: string,
215
209
  ): string {
216
- const head = hubPrefix(hub);
217
- if (!current && stats.activeTasks === 0 && stats.planCount === 0 && stats.inbox === 0) {
218
- const idle = hub?.online === undefined && hub?.state === undefined ? "kxm idle" : `${head} · idle`;
219
- const withShip = ship?.dirty
220
- ? `${idle} · dirty`
221
- : (ship && ship.ahead > 0 ? `${idle} · ${ship.ahead} local` : idle);
222
- const withCost = cost ? `${withShip} · ${cost}` : withShip;
223
- const finalLine = updateLatest ? `${withCost} · upd ${updateLatest}` : withCost;
224
- return finalLine.length <= 80 ? finalLine : `${finalLine.slice(0, 79)}…`;
225
- }
226
- const parts: string[] = [];
227
- if (current?.kind === "task") parts.push(`${head} ${current.detail}`);
228
- else if (current?.kind === "plan") parts.push(`${head} plan ${truncate(current.detail, 36)}`);
229
- else parts.push(head);
230
- parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
210
+ const parts = ["kxm"];
211
+ const hubPart = hubStatusPart(hub);
212
+ if (hubPart) parts.push(hubPart);
213
+ if (current?.kind === "task") parts.push(current.detail);
214
+ else if (current?.kind === "plan") parts.push(`plan ${truncate(current.detail, 36)}`);
215
+ if (stats.activeTasks > 0) parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
231
216
  if (stats.waitingTasks > 0) parts.push(`${stats.waitingTasks} waiting`);
232
- parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
217
+ if (stats.planCount > 0) parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
233
218
  if (stats.inbox > 0) parts.push(`inbox ${stats.inbox}`);
234
219
  if (ship?.dirty) parts.push("dirty");
235
220
  else if (ship && ship.ahead > 0) parts.push(`${ship.ahead} local`);
236
- if (cost) parts.push(cost);
237
221
  if (updateLatest) parts.push(`upd ${updateLatest}`);
238
- const line = parts.join(" · ");
239
- return line.length <= 80 ? line : `${line.slice(0, 79)}…`;
222
+ return truncate(parts.join(" · "), 80);
240
223
  }
241
224
 
242
225
  export function formatSessionWidget(
@@ -247,19 +230,22 @@ export function formatSessionWidget(
247
230
  updateLatest?: string,
248
231
  cost?: string,
249
232
  ): string[] {
250
- const hubMark = hub?.state === "on" || hub?.online === true
251
- ? "hub:on "
252
- : (hub?.state === "off" || hub?.online === false
253
- ? "hub:off "
254
- : (hub?.state === "unknown" ? "hub:unknown " : ""));
255
- const lines = [
256
- `KXM ${hubMark}${stats.activeTasks} tasks ${stats.waitingTasks} waiting ${stats.planCount} plans inbox ${stats.inbox}`,
257
- ];
233
+ const suffix = hub?.scope === "remote" ? "/remote" : "";
234
+ const hubMark = hub?.state === "on" || (hub?.state === undefined && hub?.online === true)
235
+ ? `hub:on${suffix}`
236
+ : (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)
237
+ ? `hub:off${suffix}`
238
+ : (hub?.state === "unknown" ? `hub:unknown${suffix}` : undefined));
239
+ const summary = ["KXM", hubMark];
240
+ if (stats.activeTasks > 0) summary.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
241
+ if (stats.waitingTasks > 0) summary.push(`${stats.waitingTasks} waiting`);
242
+ if (stats.planCount > 0) summary.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
243
+ if (stats.inbox > 0) summary.push(`inbox ${stats.inbox}`);
244
+ const lines = [summary.filter(Boolean).join(" · ")];
258
245
  if (current) lines.push(`now ${current.kind} ${truncate(current.detail, 56)}`);
259
- else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
260
- else lines.push("now no selected work");
261
- lines.push(formatShipLine(ship));
262
- if (cost) lines.push(`cost ${cost}`);
246
+ else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
247
+ if (ship?.dirty || (ship && ship.ahead > 0)) lines.push(formatShipLine(ship));
248
+ if (cost && cost !== "unknown") lines.push(`cost ${cost}`);
263
249
  if (updateLatest) lines.push(`update ${updateLatest} available · kxm update --kxm`);
264
250
  return lines;
265
251
  }
@@ -290,7 +276,7 @@ export function buildSessionBrief(
290
276
  const tasks = uniqueLabels(recentTasks(snapshot.runs).map(taskItem));
291
277
  const plans = uniqueLabels(snapshot.plans.slice(0, MAX_SESSION_BRIEF_PLANS).map(planItem));
292
278
  const resolvedHub: SessionHubStatus = hub ?? { state: "off", evidence: "unconfigured", online: false };
293
- const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest, cost);
279
+ const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest);
294
280
  const widgetLines = formatSessionWidget(stats, current, resolvedHub, ship, updateLatest, cost);
295
281
  return {
296
282
  schema: SESSION_BRIEF_SCHEMA,
@@ -359,7 +345,7 @@ export function estimateSessionCost(stateDir: string): string | undefined {
359
345
  latestModel = (r.effectiveModel as string | undefined) ?? (r.requestedModel as string | undefined) ?? latestModel;
360
346
  latestHarness = (r.harness as string | undefined) ?? latestHarness;
361
347
  }
362
- if (!hasMetered) return "unknown";
348
+ if (!hasMetered) return undefined;
363
349
  const route = latestHarness && latestModel ? `${latestHarness}/${latestModel}` : latestModel;
364
350
  return `$${totalCost.toFixed(2)} sess${route ? ` · ${route}` : ""}`;
365
351
  } catch {
@@ -417,7 +403,6 @@ export function loadSessionBrief(
417
403
  effectiveHub,
418
404
  options.ship,
419
405
  options.updateLatest,
420
- effectiveCost,
421
406
  );
422
407
  const widgetLines = formatSessionWidget(
423
408
  cached.stats,
@@ -499,7 +484,6 @@ export async function loadSessionBriefAsync(
499
484
  effectiveHub,
500
485
  options.ship,
501
486
  undefined,
502
- effectiveCost,
503
487
  );
504
488
  const widgetLines = formatSessionWidget(
505
489
  cached.stats,