@danypops/pi-jittor 0.2.1 → 0.3.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.
@@ -1,22 +1,33 @@
1
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  import { JITTOR_EXTENSION_SETTINGS_FILENAME, JITTOR_STATE_DIRECTORY, USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
5
+ import { createAtomicJsonWriter } from "@danypops/vehicle-core";
6
+ import { createNodeAtomicJsonFsAdapter } from "@danypops/vehicle-server/atomic-json";
4
7
 
8
+ const atomicJson = createAtomicJsonWriter({ fs: createNodeAtomicJsonFsAdapter() });
9
+
10
+ // Every setter persists to disk before resolving, so a caller that awaits it (every real call
11
+ // site in index.ts/settings-tui.ts already runs inside an async handler) observes a completed
12
+ // write -- no fire-and-forget persistence that a subsequent synchronous re-read could race. The
13
+ // return type stays `void | Promise<void>` (not a strict `Promise<void>`) so a test/adapter
14
+ // implementation that has no real persistence to await (e.g. the no-op fallback stubs in
15
+ // index.ts's usageBudgetControl()/recoveryControl()) can stay trivially synchronous.
5
16
  export interface EnforcementControl {
6
17
  isEnabled(): boolean;
7
- setEnabled(enabled: boolean): void;
18
+ setEnabled(enabled: boolean): void | Promise<void>;
8
19
  isFooterEnabled(): boolean;
9
- setFooterEnabled(enabled: boolean): void;
20
+ setFooterEnabled(enabled: boolean): void | Promise<void>;
10
21
  }
11
22
 
12
23
  export interface CodexRecoveryControl {
13
24
  isCodexRecoveryEnabled(): boolean;
14
- setCodexRecoveryEnabled(enabled: boolean): void;
25
+ setCodexRecoveryEnabled(enabled: boolean): void | Promise<void>;
15
26
  }
16
27
 
17
28
  export interface UsageBudgetControl {
18
29
  getUsageTokenBudget(period: UsagePeriod): number | undefined;
19
- setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void;
30
+ setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void | Promise<void>;
20
31
  }
21
32
 
22
33
  export interface PersistentExtensionControl extends EnforcementControl, CodexRecoveryControl, UsageBudgetControl {}
@@ -64,12 +75,9 @@ function loadSettings(path: string): ExtensionSettings {
64
75
  }
65
76
  }
66
77
 
67
- function persistSettings(path: string, settings: ExtensionSettings): void {
68
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
69
- const temporary = `${path}.${process.pid}.tmp`;
70
- writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
71
- chmodSync(temporary, 0o600);
72
- renameSync(temporary, path);
78
+ async function persistSettings(path: string, settings: ExtensionSettings): Promise<void> {
79
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
80
+ await atomicJson.write(path, settings, { mode: 0o600, pretty: true, trailingNewline: true });
73
81
  }
74
82
 
75
83
  export function persistentEnforcementControl(env: Record<string, string | undefined> = process.env): PersistentExtensionControl {
@@ -77,29 +85,29 @@ export function persistentEnforcementControl(env: Record<string, string | undefi
77
85
  const settings = loadSettings(path);
78
86
  return {
79
87
  isEnabled: () => settings.enforcementEnabled,
80
- setEnabled(value: boolean): void {
88
+ async setEnabled(value: boolean): Promise<void> {
81
89
  settings.enforcementEnabled = value;
82
- persistSettings(path, settings);
90
+ await persistSettings(path, settings);
83
91
  },
84
92
  isFooterEnabled: () => settings.footerEnabled,
85
- setFooterEnabled(value: boolean): void {
93
+ async setFooterEnabled(value: boolean): Promise<void> {
86
94
  settings.footerEnabled = value;
87
- persistSettings(path, settings);
95
+ await persistSettings(path, settings);
88
96
  },
89
97
  isCodexRecoveryEnabled: () => settings.codexRecoveryEnabled,
90
- setCodexRecoveryEnabled(value: boolean): void {
98
+ async setCodexRecoveryEnabled(value: boolean): Promise<void> {
91
99
  settings.codexRecoveryEnabled = value;
92
- persistSettings(path, settings);
100
+ await persistSettings(path, settings);
93
101
  },
94
102
  getUsageTokenBudget(period): number | undefined {
95
103
  return settings.usageTokenBudgets[period];
96
104
  },
97
- setUsageTokenBudget(period, tokens): void {
105
+ async setUsageTokenBudget(period, tokens): Promise<void> {
98
106
  if (tokens !== undefined && (!Number.isFinite(tokens) || tokens <= 0))
99
107
  throw new Error("usage token budget must be a positive finite number");
100
108
  if (tokens === undefined) delete settings.usageTokenBudgets[period];
101
109
  else settings.usageTokenBudgets[period] = tokens;
102
- persistSettings(path, settings);
110
+ await persistSettings(path, settings);
103
111
  },
104
112
  };
105
113
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danypops/pi-jittor",
3
- "version": "0.2.1",
4
- "description": "Pi extension for Jittor: native routing enforcement, footer, settings, usage graphs, and benchmark panels backed by the @danypops/jittor daemon",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension for Jittor token and context observability with optimization controls backed by the @danypops/jittor daemon",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "llm-router", "token-budget"],
7
7
  "scripts": {
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@danypops/vehicle-client": "^0.5.1",
16
- "@danypops/jittor": "^0.14.0",
16
+ "@danypops/vehicle-core": "^0.12.1",
17
+ "@danypops/vehicle-server": "^0.17.0",
18
+ "@danypops/jittor": "workspace:^",
17
19
  "malevich-tui-components": "^0.16.1"
18
20
  },
19
21
  "peerDependencies": {
@@ -22,6 +24,10 @@
22
24
  "typebox": "*"
23
25
  },
24
26
  "devDependencies": {
27
+ "@danypops/pi-extension-harness": "^0.6.2",
28
+ "@danypops/pi-process-harness": "^0.1.2",
29
+ "@danypops/pi-tui-harness": "^0.0.2",
30
+ "@earendil-works/pi-ai": "*",
25
31
  "bun-types": "latest"
26
32
  },
27
33
  "repository": {
@@ -1,140 +0,0 @@
1
- import type { ContextSegment } from "@danypops/jittor";
2
- import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
3
- import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
4
- import {
5
- buildContextRows,
6
- type ContextBarTheme,
7
- type ContextRow,
8
- type ContextRowsTheme,
9
- type ContextSegment as MalevichContextSegment,
10
- renderContextRowLines,
11
- renderContextUsageBar,
12
- } from "malevich-tui-components";
13
- import type { ContextBreakdown } from "./context-breakdown.ts";
14
- import { buildContextReport } from "./context-report.ts";
15
-
16
- const VISIBLE_ROWS = 24;
17
-
18
- /**
19
- * A dynamically-contributed segment set (any string key from any extension, not a fixed enum)
20
- * can't use a hardcoded per-key color map the way Papyrus's own ContextViewport did for its
21
- * fixed seven segments -- this cycles a small categorical palette keyed by a stable hash of the
22
- * segment key, so the same key always renders the same color within one process without needing
23
- * every possible contributor's key to be known in advance.
24
- */
25
- const PALETTE: ThemeColor[] = ["accent", "success", "syntaxFunction", "warning", "syntaxKeyword", "syntaxType", "muted"];
26
-
27
- function paletteColor(key: string): ThemeColor {
28
- let hash = 0;
29
- for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
30
- return PALETTE[hash % PALETTE.length]!;
31
- }
32
-
33
- function formatTokenCount(tokens: number): string {
34
- return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
35
- }
36
-
37
- function percentOf(part: number, whole: number): string {
38
- return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
39
- }
40
-
41
- /** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
42
- function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
43
- return {
44
- key: segment.key,
45
- label: `${segment.label} [${segment.confidence}]`,
46
- estimatedTokens: segment.estimatedTokens,
47
- items: segment.items,
48
- unknown: segment.unknown,
49
- };
50
- }
51
-
52
- class ContextViewport {
53
- private offsetY = 0;
54
- private readonly rows: ContextRow[];
55
- private readonly segments: readonly MalevichContextSegment[];
56
-
57
- constructor(
58
- private readonly tui: TUI,
59
- private readonly theme: Theme,
60
- private readonly breakdown: ContextBreakdown,
61
- private readonly close: () => void,
62
- ) {
63
- // Heaviest-first: Malevich renders segments in the order given, so sorting by weight for a
64
- // merged multi-producer view is this viewport's own policy, matching context-report.ts.
65
- this.segments = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
66
- this.rows = buildContextRows(this.segments, breakdown.totalTokens ?? undefined);
67
- }
68
-
69
- invalidate(): void {}
70
-
71
- render(width: number): string[] {
72
- const theme = this.theme;
73
- const contentWidth = Math.max(1, width);
74
- const border = theme.fg("borderMuted", "─".repeat(contentWidth));
75
- const lines: string[] = [border, truncateToWidth(theme.fg("accent", theme.bold("Context")), contentWidth, "")];
76
-
77
- const { totalTokens, effectiveBudget } = this.breakdown;
78
- if (totalTokens !== null && effectiveBudget !== null) {
79
- lines.push(
80
- truncateToWidth(
81
- `${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`,
82
- contentWidth,
83
- "",
84
- ),
85
- );
86
- } else if (totalTokens !== null) {
87
- lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
88
- } else {
89
- lines.push(theme.fg("dim", "No real usage reported yet — sizes below are estimates only"));
90
- }
91
-
92
- const colorFor = (key: string) => (s: string) => theme.fg(paletteColor(key), s);
93
- const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
94
- lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
95
- if (this.breakdown.overshootTokens > 0) {
96
- lines.push(
97
- truncateToWidth(
98
- theme.fg(
99
- "warning",
100
- `Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`,
101
- ),
102
- contentWidth,
103
- "",
104
- ),
105
- );
106
- }
107
- lines.push("");
108
-
109
- const rowsTheme: ContextRowsTheme = { colorFor, header: (s) => theme.bold(s) };
110
- const visible = this.rows.slice(this.offsetY, this.offsetY + VISIBLE_ROWS);
111
- lines.push(...renderContextRowLines(visible, contentWidth, rowsTheme));
112
- if (this.rows.length === 0) lines.push(theme.fg("dim", " (nothing observed yet)"));
113
- else lines.push(theme.fg("muted", ` ${Math.min(this.offsetY + VISIBLE_ROWS, this.rows.length)}/${this.rows.length}`));
114
-
115
- lines.push("");
116
- lines.push(theme.fg("dim", "↑↓ scroll · esc close"));
117
- lines.push(border);
118
- return lines;
119
- }
120
-
121
- handleInput(data: string): void {
122
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
123
- this.close();
124
- return;
125
- }
126
- if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
127
- else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.rows.length - VISIBLE_ROWS), this.offsetY + 1);
128
- else return;
129
- this.tui.requestRender();
130
- }
131
- }
132
-
133
- /** Interactive scrollable Context Hub view in TUI mode; the same plain-text report as /context's non-interactive path otherwise. */
134
- export async function showContextView(ctx: ExtensionCommandContext, breakdown: ContextBreakdown): Promise<void> {
135
- if (ctx.mode !== "tui") {
136
- ctx.ui.notify(buildContextReport(breakdown), "info");
137
- return;
138
- }
139
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new ContextViewport(tui, theme, breakdown, done));
140
- }