@danypops/jittor 0.5.1 → 0.7.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 (44) hide show
  1. package/README.md +62 -7
  2. package/docs/BENCHMARK_SOURCES.md +32 -0
  3. package/docs/OUTPUT_CHANNELS.md +31 -0
  4. package/docs/PROVIDER_RESEARCH.md +57 -0
  5. package/extension/src/benchmark-tui.ts +105 -0
  6. package/extension/src/footer.ts +68 -12
  7. package/extension/src/index.ts +263 -36
  8. package/extension/src/tui.ts +61 -9
  9. package/extension/src/usage.ts +165 -47
  10. package/package.json +5 -1
  11. package/src/adapters/metric-benchmark-store.ts +99 -0
  12. package/src/adapters/openrouter-benchmark-index-source.ts +94 -0
  13. package/src/adapters/openrouter-benchmark-source.ts +109 -0
  14. package/src/adapters/sqlite-metric-store.ts +43 -2
  15. package/src/cli.ts +660 -9
  16. package/src/client.ts +12 -44
  17. package/src/constants.ts +69 -5
  18. package/src/daemon.ts +65 -43
  19. package/src/db.ts +13 -30
  20. package/src/domain/benchmark.ts +264 -0
  21. package/src/domain/context-telemetry.ts +31 -0
  22. package/src/domain/metric.ts +46 -5
  23. package/src/domain/model-observation.ts +203 -0
  24. package/src/domain/model-ranking-service.ts +41 -0
  25. package/src/domain/model-ranking.ts +232 -0
  26. package/src/domain/task-cost.ts +70 -0
  27. package/src/domain/task-focus.ts +65 -0
  28. package/src/domain/usage.ts +134 -33
  29. package/src/log.ts +28 -0
  30. package/src/ports/benchmark-controller.ts +11 -0
  31. package/src/ports/benchmark-source.ts +7 -0
  32. package/src/ports/benchmark-store.ts +7 -0
  33. package/src/ports/metric-store.ts +30 -0
  34. package/src/ports/router-controller.ts +1 -0
  35. package/src/providers/anthropic-contracts.ts +127 -0
  36. package/src/providers/google-adc-auth.ts +63 -0
  37. package/src/providers/google-vertex-budget-contracts.ts +181 -0
  38. package/src/providers/google-vertex-budget.ts +127 -0
  39. package/src/providers/google-vertex-contracts.ts +116 -0
  40. package/src/providers/telemetry-sources.ts +35 -0
  41. package/src/router.ts +12 -0
  42. package/src/service.ts +132 -17
  43. package/src/state.ts +31 -57
  44. package/src/version.ts +2 -14
package/src/service.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS, CONTEXT_ASSESSMENT_QUERY_LIMIT, SERVICE_MAX_BODY_BYTES } from "./constants.ts";
1
+ import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/daemon-kit/http";
2
+ import { COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES, CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS, CONTEXT_ASSESSMENT_QUERY_LIMIT, MAX_USAGE_BUCKETS, PRUNE_MIN_AGE_MS, SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES, TASK_COST_QUERY_LIMIT, USAGE_MAX_DISTINCT_SCOPES } from "./constants.ts";
2
3
  import { VERSION } from "./version.ts";
3
4
  import { validateMetricObservation, type MetricObservation, type MetricQuery, type StoredMetricObservation } from "./domain/metric.ts";
4
- import { assessContextTelemetry, type ContextAssessment } from "./domain/context-telemetry.ts";
5
+ import { assessContextTelemetry, estimateCompactionDuration, type CompactionDurationEstimate, type ContextAssessment } from "./domain/context-telemetry.ts";
6
+ import { buildTaskCostSummary, type TaskCostSummary } from "./domain/task-cost.ts";
7
+ import type { UsageAggregateRow } from "./domain/usage.ts";
8
+ import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
9
+ import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
10
+ import type { ModelRankingResult } from "./domain/model-ranking.ts";
11
+ import type { BenchmarkController } from "./ports/benchmark-controller.ts";
5
12
  import type { MetricStore } from "./ports/metric-store.ts";
6
13
  import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult } from "./ports/router-controller.ts";
7
14
  import type { PolicyDecision, Route } from "./policy.ts";
@@ -9,8 +16,16 @@ import type { PolicyDecision, Route } from "./policy.ts";
9
16
  export const EXPECTED_OPERATION_NAMES = [
10
17
  "metrics.record",
11
18
  "metrics.query",
19
+ "metrics.distinct_scopes",
20
+ "metrics.usage_series",
21
+ "metrics.cost_by_task",
12
22
  "metrics.prune",
23
+ "benchmark.refresh",
24
+ "benchmark.status",
25
+ "benchmark.query",
26
+ "models.rank",
13
27
  "context.assess",
28
+ "compaction.estimate",
14
29
  "service.checkpoint",
15
30
  "telemetry.poll",
16
31
  "router.status",
@@ -27,8 +42,16 @@ export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
27
42
  export interface OperationInputs {
28
43
  "metrics.record": MetricObservation;
29
44
  "metrics.query": MetricQuery;
30
- "metrics.prune": { before: number };
45
+ "metrics.distinct_scopes": { source: string; since: number; until: number; limit?: number };
46
+ "metrics.usage_series": { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number };
47
+ "metrics.cost_by_task": { since: number; until: number };
48
+ "metrics.prune": { before: number; force?: boolean };
49
+ "benchmark.refresh": { force?: boolean };
50
+ "benchmark.status": Record<string, never>;
51
+ "benchmark.query": BenchmarkQuery;
52
+ "models.rank": ModelRecommendationInput;
31
53
  "context.assess": { since?: number; until?: number };
54
+ "compaction.estimate": Record<string, never>;
32
55
  "service.checkpoint": Record<string, never>;
33
56
  "telemetry.poll": Record<string, never>;
34
57
  "router.status": Record<string, never>;
@@ -43,8 +66,16 @@ export interface OperationInputs {
43
66
  export interface OperationOutputs {
44
67
  "metrics.record": StoredMetricObservation;
45
68
  "metrics.query": StoredMetricObservation[];
69
+ "metrics.distinct_scopes": string[];
70
+ "metrics.usage_series": { rows: UsageAggregateRow[]; truncated: boolean };
71
+ "metrics.cost_by_task": TaskCostSummary;
46
72
  "metrics.prune": { deleted: number };
73
+ "benchmark.refresh": BenchmarkRefreshResult;
74
+ "benchmark.status": BenchmarkRefreshResult;
75
+ "benchmark.query": BenchmarkQueryResult;
76
+ "models.rank": ModelRankingResult;
47
77
  "context.assess": ContextAssessment;
78
+ "compaction.estimate": CompactionDurationEstimate;
48
79
  "service.checkpoint": { ok: true };
49
80
  "telemetry.poll": TelemetryPollResult;
50
81
  "router.status": RouterStatus;
@@ -59,6 +90,16 @@ export interface OperationOutputs {
59
90
 
60
91
  export class UnknownOperationError extends Error {}
61
92
 
93
+ class UnavailableModelRanker implements ModelRanker {
94
+ rank(): ModelRankingResult { throw new Error("model ranking is not configured"); }
95
+ }
96
+
97
+ class UnavailableBenchmarkController implements BenchmarkController {
98
+ async refresh(): Promise<BenchmarkRefreshResult> { return this.status(); }
99
+ status(): BenchmarkRefreshResult { return { observedAt: Date.now(), sources: [] }; }
100
+ query(): BenchmarkQueryResult { throw new Error("benchmark evidence is not configured"); }
101
+ }
102
+
62
103
  class UnavailableRouter implements RouterController {
63
104
  private readonly unavailable: RouterStatus = { ready: false, paused: false, sources: [], lastDecision: null, override: null, currentRoute: null, availableRoutes: [] };
64
105
  async poll(): Promise<TelemetryPollResult> { return { sources: [], observedAt: Date.now() }; }
@@ -76,6 +117,8 @@ export class JittorService {
76
117
  constructor(
77
118
  private readonly metrics: MetricStore,
78
119
  private readonly router: RouterController = new UnavailableRouter(),
120
+ private readonly benchmarks: BenchmarkController = new UnavailableBenchmarkController(),
121
+ private readonly modelRanker: ModelRanker = new UnavailableModelRanker(),
79
122
  ) {}
80
123
 
81
124
  operationNames(): OperationName[] {
@@ -88,11 +131,71 @@ export class JittorService {
88
131
  switch (operation) {
89
132
  case "metrics.record": return this.metrics.record(validateMetricObservation(input));
90
133
  case "metrics.query": return this.metrics.query(input as MetricQuery);
134
+ case "metrics.distinct_scopes": {
135
+ const source = input["source"];
136
+ const since = input["since"];
137
+ const until = input["until"];
138
+ if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
139
+ if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
140
+ throw new Error("distinct scopes requires non-negative ordered integer bounds");
141
+ }
142
+ const requestedLimit = input["limit"];
143
+ const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
144
+ return this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit });
145
+ }
146
+ case "metrics.usage_series": {
147
+ const source = input["source"];
148
+ const since = input["since"];
149
+ const until = input["until"];
150
+ const bucketSizeMs = input["bucketSizeMs"];
151
+ const bucketCount = input["bucketCount"];
152
+ if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
153
+ if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
154
+ throw new Error("usage series requires non-negative ordered integer bounds");
155
+ }
156
+ if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0) throw new Error("bucketSizeMs must be a positive number");
157
+ if (!Number.isInteger(bucketCount) || (bucketCount as number) <= 0 || (bucketCount as number) > MAX_USAGE_BUCKETS) {
158
+ throw new Error(`bucketCount must be a positive integer up to ${MAX_USAGE_BUCKETS}`);
159
+ }
160
+ const requestedScopeLimit = input["scopeLimit"];
161
+ const scopeLimit = Number.isFinite(requestedScopeLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedScopeLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
162
+ const scopes = this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit: scopeLimit });
163
+ // More distinct scopes may exist beyond this bounded list -- that is the only remaining
164
+ // truncation risk once aggregation replaces a per-scope raw-row fetch (see aggregateUsage's
165
+ // own doc comment for the incident this was built to stop repeating).
166
+ const truncated = scopes.length >= scopeLimit;
167
+ const rows = scopes.length === 0 ? [] : this.metrics.aggregateUsage({
168
+ source, scopes, since: since as number, until: until as number, bucketSizeMs, bucketCount: bucketCount as number,
169
+ });
170
+ return { rows, truncated };
171
+ }
172
+ case "metrics.cost_by_task": {
173
+ const since = input["since"];
174
+ const until = input["until"];
175
+ if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
176
+ throw new Error("cost by task requires non-negative ordered integer bounds");
177
+ }
178
+ const rows = this.metrics.query({ source: "pi", since: since as number, until: until as number, order: "desc", limit: TASK_COST_QUERY_LIMIT });
179
+ return buildTaskCostSummary(rows, { since: since as number, until: until as number, truncated: rows.length >= TASK_COST_QUERY_LIMIT });
180
+ }
91
181
  case "metrics.prune": {
92
182
  const before = input["before"];
93
183
  if (typeof before !== "number") throw new Error("before is required");
184
+ const force = input["force"] === true;
185
+ const minCutoff = Date.now() - PRUNE_MIN_AGE_MS;
186
+ if (!force && before > minCutoff) {
187
+ throw new Error(`refusing to prune metrics newer than ${new Date(minCutoff).toISOString()} without force: true (this looked like it could delete recent or live data)`);
188
+ }
94
189
  return { deleted: this.metrics.pruneBefore(before) };
95
190
  }
191
+ case "benchmark.refresh": return this.benchmarks.refresh(input["force"] === true);
192
+ case "benchmark.status": return this.benchmarks.status();
193
+ case "benchmark.query": return this.benchmarks.query(input as unknown as BenchmarkQuery);
194
+ case "models.rank": {
195
+ const result = this.modelRanker.rank(input as unknown as ModelRecommendationInput);
196
+ if (result.automaticSelection && this.router.applyModelRanking) this.router.applyModelRanking(result.ranked.map((item) => item.candidate));
197
+ return result;
198
+ }
96
199
  case "context.assess": {
97
200
  const until = input["until"] === undefined ? Date.now() : input["until"];
98
201
  const since = input["since"] === undefined && typeof until === "number" ? Math.max(0, until - CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS) : input["since"];
@@ -106,6 +209,13 @@ export class JittorService {
106
209
  truncated: injections.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT || compactions.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT,
107
210
  });
108
211
  }
212
+ case "compaction.estimate": {
213
+ const rows = this.metrics.query({
214
+ source: "pi-context", scope: "compaction", metric: "compaction-duration",
215
+ order: "desc", limit: COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES,
216
+ });
217
+ return estimateCompactionDuration(rows);
218
+ }
109
219
  case "service.checkpoint": this.metrics.checkpoint(); return { ok: true };
110
220
  case "telemetry.poll": return this.router.poll();
111
221
  case "router.status": return this.router.status();
@@ -135,31 +245,36 @@ export interface JittorAppOptions {
135
245
  maxBodyBytes?: number;
136
246
  }
137
247
 
138
- function authorized(request: Request, token: string): boolean {
139
- return request.headers.get("authorization") === `Bearer ${token}`;
140
- }
141
-
248
+ /**
249
+ * Bearer-check and the trivial health/ready/not-found responses now delegate to
250
+ * `@danypops/daemon-kit/http` (the same handful of lines every daemon's service.ts hand-rolled).
251
+ * The response-size guard below stays jittor-specific: daemon-kit's `jsonResponse` is intentionally
252
+ * unbounded (it has no operation dispatch of its own to guard), while jittor's `/api/v1/ops` can
253
+ * return arbitrarily large query results that must be capped (see SERVICE_MAX_RESPONSE_BYTES).
254
+ */
142
255
  function json(value: unknown, status = 200): Response {
143
- return Response.json(value, { status });
256
+ const body = JSON.stringify(value);
257
+ if (new TextEncoder().encode(body).byteLength > SERVICE_MAX_RESPONSE_BYTES) return errorResponse("response too large", 413);
258
+ return new Response(body, {
259
+ status,
260
+ headers: { "content-type": "application/json", "content-length": String(new TextEncoder().encode(body).byteLength) },
261
+ });
144
262
  }
145
263
 
146
264
  export function createApp(options: JittorAppOptions): { fetch(request: Request): Promise<Response> } {
147
265
  const maxBodyBytes = options.maxBodyBytes ?? SERVICE_MAX_BODY_BYTES;
148
266
  return {
149
267
  async fetch(request: Request): Promise<Response> {
150
- if (!authorized(request, options.token)) return json({ error: "unauthorized" }, 401);
268
+ if (!requireBearerToken(request, options.token)) return errorResponse("unauthorized", 401);
151
269
  const url = new URL(request.url);
152
- if (request.method === "GET" && url.pathname === "/health") return json({ ok: true, version: VERSION });
153
- if (request.method === "GET" && url.pathname === "/ready") {
154
- const ready = options.service.ready();
155
- return json({ ready }, ready ? 200 : 503);
156
- }
270
+ if (request.method === "GET" && url.pathname === "/health") return healthResponse(VERSION);
271
+ if (request.method === "GET" && url.pathname === "/ready") return readyResponse(options.service.ready());
157
272
  if (request.method === "GET" && url.pathname === "/api/v1/ops") return json({ operations: options.service.operationNames() });
158
- if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return json({ error: "not found" }, 404);
273
+ if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return errorResponse("not found", 404);
159
274
  const contentLength = Number(request.headers.get("content-length") ?? 0);
160
- if (contentLength > maxBodyBytes) return json({ error: "payload too large" }, 413);
275
+ if (contentLength > maxBodyBytes) return errorResponse("payload too large", 413);
161
276
  const text = await request.text();
162
- if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return json({ error: "payload too large" }, 413);
277
+ if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return errorResponse("payload too large", 413);
163
278
  try {
164
279
  const body = JSON.parse(text) as { op?: unknown; input?: unknown };
165
280
  if (typeof body.op !== "string") throw new Error("op is required");
package/src/state.ts CHANGED
@@ -1,81 +1,55 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { randomBytes } from "node:crypto";
1
+ /**
2
+ * Jittor's XDG paths/token/handle layout, now delegating to `@danypops/daemon-kit/paths` --
3
+ * the shared substrate factored out after jittor's own state.ts and web-spider-daemon's were
4
+ * found byte-identical (see daemon-kit's README). Kept as a thin jittor-named wrapper (same
5
+ * exported function names/signatures as before) so every existing call site (daemon.ts,
6
+ * client.ts, cli.ts, and their tests) is untouched by this migration.
7
+ */
8
+ import {
9
+ ensureAuthToken as ensureDaemonKitAuthToken,
10
+ readDaemonHandle as readDaemonKitHandle,
11
+ removeDaemonHandle as removeDaemonKitHandle,
12
+ resolveDaemonPaths,
13
+ writeDaemonHandle as writeDaemonKitHandle,
14
+ type DaemonHandle,
15
+ type DaemonPaths,
16
+ type PathEnvironment,
17
+ } from "@danypops/daemon-kit/paths";
5
18
  import {
6
19
  DATABASE_FILENAME,
7
20
  HANDLE_FILENAME,
8
21
  JITTOR_STATE_DIRECTORY,
9
- LOOPBACK_HOST,
10
22
  SYSTEMD_UNIT_NAME,
11
23
  TOKEN_FILENAME,
12
24
  } from "./constants.ts";
13
25
 
14
- export interface JittorPaths {
15
- database: string;
16
- token: string;
17
- handle: string;
18
- systemdUnit: string;
19
- }
26
+ export type JittorPaths = DaemonPaths;
27
+ export type { DaemonHandle, PathEnvironment };
20
28
 
21
- export interface DaemonHandle {
22
- host: typeof LOOPBACK_HOST;
23
- port: number;
24
- pid: number;
25
- }
26
-
27
- export interface PathEnvironment {
28
- env?: Record<string, string | undefined>;
29
- home?: string;
30
- uid?: number;
31
- }
29
+ const JITTOR_PATH_NAMES = {
30
+ stateDirectoryName: JITTOR_STATE_DIRECTORY,
31
+ databaseFilename: DATABASE_FILENAME,
32
+ tokenFilename: TOKEN_FILENAME,
33
+ handleFilename: HANDLE_FILENAME,
34
+ systemdUnitName: SYSTEMD_UNIT_NAME,
35
+ };
32
36
 
33
37
  export function resolveJittorPaths(options: PathEnvironment = {}): JittorPaths {
34
- const env = options.env ?? process.env;
35
- const home = options.home ?? homedir();
36
- const uid = options.uid ?? process.getuid?.() ?? 0;
37
- const dataHome = env["XDG_DATA_HOME"] ?? join(home, ".local", "share");
38
- const stateHome = env["XDG_STATE_HOME"] ?? join(home, ".local", "state");
39
- const runtimeHome = env["XDG_RUNTIME_DIR"] ?? join("/run", "user", String(uid));
40
- const configHome = env["XDG_CONFIG_HOME"] ?? join(home, ".config");
41
- return {
42
- database: join(dataHome, JITTOR_STATE_DIRECTORY, DATABASE_FILENAME),
43
- token: join(stateHome, JITTOR_STATE_DIRECTORY, TOKEN_FILENAME),
44
- handle: join(runtimeHome, JITTOR_STATE_DIRECTORY, HANDLE_FILENAME),
45
- systemdUnit: join(configHome, "systemd", "user", SYSTEMD_UNIT_NAME),
46
- };
38
+ return resolveDaemonPaths(JITTOR_PATH_NAMES, options);
47
39
  }
48
40
 
49
41
  export function ensureAuthToken(paths: JittorPaths = resolveJittorPaths()): string {
50
- mkdirSync(dirname(paths.token), { recursive: true, mode: 0o700 });
51
- if (existsSync(paths.token)) {
52
- chmodSync(paths.token, 0o600);
53
- const token = readFileSync(paths.token, "utf8").trim();
54
- if (!/^[a-f0-9]{64}$/.test(token)) throw new Error("invalid Jittor authentication token");
55
- return token;
56
- }
57
- const token = randomBytes(32).toString("hex");
58
- writeFileSync(paths.token, `${token}\n`, { mode: 0o600 });
59
- return token;
42
+ return ensureDaemonKitAuthToken(paths.token, "Jittor");
60
43
  }
61
44
 
62
45
  export function writeDaemonHandle(paths: JittorPaths, handle: DaemonHandle): void {
63
- mkdirSync(dirname(paths.handle), { recursive: true, mode: 0o700 });
64
- const temporary = `${paths.handle}.${process.pid}.tmp`;
65
- writeFileSync(temporary, `${JSON.stringify(handle)}\n`, { mode: 0o600 });
66
- renameSync(temporary, paths.handle);
46
+ writeDaemonKitHandle(paths.handle, handle);
67
47
  }
68
48
 
69
49
  export function readDaemonHandle(paths: JittorPaths = resolveJittorPaths()): DaemonHandle | null {
70
- try {
71
- const value = JSON.parse(readFileSync(paths.handle, "utf8")) as Partial<DaemonHandle>;
72
- if (value.host !== LOOPBACK_HOST || !Number.isInteger(value.port) || value.port! < 1 || value.port! > 65_535 || !Number.isInteger(value.pid)) return null;
73
- return value as DaemonHandle;
74
- } catch {
75
- return null;
76
- }
50
+ return readDaemonKitHandle(paths.handle);
77
51
  }
78
52
 
79
53
  export function removeDaemonHandle(paths: JittorPaths = resolveJittorPaths()): void {
80
- rmSync(paths.handle, { force: true });
54
+ removeDaemonKitHandle(paths.handle);
81
55
  }
package/src/version.ts CHANGED
@@ -1,16 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- function packageVersion(): string {
4
- const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as unknown;
5
- if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
6
- throw new Error("Jittor package manifest must be an object");
7
- }
8
- const version = (manifest as Record<string, unknown>)["version"];
9
- if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
10
- throw new Error("Jittor package manifest has an invalid version");
11
- }
12
- return version;
13
- }
1
+ import { readPackageVersion } from "@danypops/daemon-kit/version";
14
2
 
15
3
  /** Runtime package version; package.json is the single release source of truth. */
16
- export const VERSION = packageVersion();
4
+ export const VERSION = readPackageVersion(new URL("../package.json", import.meta.url), "Jittor");