@remodex/rmx 1.0.5 → 1.0.6

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.
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-B-jlbgno.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-1SDbgh2-.css">
19
+ <script type="module" crossorigin src="/assets/index-BOZwc-1w.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-s2Il3gAr.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remodex/rmx",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Remodex universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "displayName": "Remodex",
6
6
  "type": "module",
package/src/cli/index.ts CHANGED
@@ -53,7 +53,6 @@ import { installShellHook, uninstallShellHook } from "../server/system-env";
53
53
  import { startTokenGuardian } from "../oauth/token-guardian";
54
54
  import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
55
55
  import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
56
- import { maybeShowStarPrompt } from "./star-prompt";
57
56
  import { scheduleCatalogPrewarm } from "./catalog-prewarm";
58
57
  import { maybeShowUpdatePrompt } from "../update/notify";
59
58
  import { syncModelsToCodex } from "../codex/sync";
@@ -399,7 +398,6 @@ async function handleStart(options: { block?: boolean } = {}) {
399
398
  // Auto-install .zshrc hook (idempotent — skips if already present).
400
399
  installShellHook();
401
400
 
402
- await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
403
401
  // Post-startup sync drives the readiness gate AND the #1046 stale app-server
404
402
  // warning. `syncCodexOnStartIfEnabled` respects the Codex integration toggle
405
403
  // (OFF → no sync) and reports whether anything was written; the readiness gate
@@ -343,11 +343,19 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
343
343
 
344
344
  if (url.pathname === "/api/update/check" && req.method === "GET") {
345
345
  const { checkForUpdate, normalizeUpdateChannel } = await import("../../update/job");
346
+ const { fetchReleaseNotesForVersion } = await import("../../update/release-notes");
346
347
  const rawTag = url.searchParams.get("tag");
347
348
  if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
348
349
  return jsonResponse({ error: "tag must be latest or preview" }, 400);
349
350
  }
350
- return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag)));
351
+ const check = checkForUpdate(normalizeUpdateChannel(rawTag));
352
+ const releaseNotesVersion = check.latestVersion ?? check.currentVersion;
353
+ const releaseNotes = await fetchReleaseNotesForVersion(releaseNotesVersion);
354
+ return jsonResponse({
355
+ ...check,
356
+ releaseNotesVersion,
357
+ ...(releaseNotes ? { releaseNotes } : {}),
358
+ });
351
359
  }
352
360
 
353
361
  if (url.pathname === "/api/update/run" && req.method === "POST") {
package/src/service.ts CHANGED
@@ -53,7 +53,6 @@ import { defaultWinswEntry, installWinswService, startWinswService, stopWinswSer
53
53
  import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
54
54
  import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
55
55
  import { rebaseConfigOwnershipRoot, recordOwnedConfigPath } from "./lib/config-ownership";
56
- import { maybeShowStarPrompt } from "./cli/star-prompt";
57
56
  import {
58
57
  type DefaultRemodexHomeResolution,
59
58
  LEGACY_REMODEX_HOME_DIRNAME,
@@ -2916,11 +2915,6 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2916
2915
  // installed artifact instead.
2917
2916
  await reportServiceServing("installed", { port: resolveServiceListenPort() });
2918
2917
  if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
2919
- // Service users never reach the `rmx start` prompt: the proxy they run is the
2920
- // supervised child, which always carries OCX_SERVICE=1. This command, though, is
2921
- // hand-typed in a real terminal, so it is the one interactive moment they get.
2922
- // Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
2923
- await maybeShowStarPrompt();
2924
2918
  break;
2925
2919
  case "start":
2926
2920
  prepareServiceProviderEnvironment();
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { atomicWriteFile, getConfigDir } from "../config";
6
- import { hasStarPromptRun } from "../cli/star-prompt";
7
6
  import {
8
7
  type Channel,
9
8
  currentVersion,
@@ -120,7 +119,7 @@ export function isSourceBuildVersion(v: string): boolean {
120
119
  return v.trim() === "0.0.0";
121
120
  }
122
121
 
123
- /** The interactive/TTY + install-method gate shared with the star prompt. */
122
+ /** The interactive/TTY + install-method gate for update notifications. */
124
123
  function interactiveGuardOk(): boolean {
125
124
  return !(process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY);
126
125
  }
@@ -128,15 +127,13 @@ function interactiveGuardOk(): boolean {
128
127
  /**
129
128
  * Decide whether this run should even consider showing the prompt. Returns the
130
129
  * channel + current version when eligible, else null. Eligibility requires a
131
- * real global install, a non-source version, the interactive guard, and that
132
- * the one-time star prompt has already run (first-run yield, O1).
130
+ * real global install, a non-source version, and the interactive guard.
133
131
  */
134
132
  export function shouldConsider(): { channel: Channel; current: string } | null {
135
133
  if (detectInstall() === "source") return null;
136
134
  const current = currentVersion();
137
135
  if (current === "?" || isSourceBuildVersion(current)) return null;
138
136
  if (!interactiveGuardOk()) return null;
139
- if (!hasStarPromptRun()) return null; // yield on the very first run
140
137
  return { channel: updateTag(current), current };
141
138
  }
142
139
 
@@ -0,0 +1,111 @@
1
+ import { readBoundedResponseBody } from "../lib/bounded-body";
2
+
3
+ /** Public repository that carries the release notes for the npm package. */
4
+ export const RELEASE_NOTES_REPOSITORY = "ESCANOR-001/remodex-android";
5
+ const RELEASE_NOTES_API_URL = `https://api.github.com/repos/${RELEASE_NOTES_REPOSITORY}/releases/tags`;
6
+ export const RELEASE_NOTES_MAX_BYTES = 32 * 1024;
7
+ const RELEASE_RESPONSE_MAX_BYTES = 64 * 1024;
8
+ const FETCH_TIMEOUT_MS = 6_000;
9
+ const BODY_TIMEOUT_MS = 5_000;
10
+ const CACHE_TTL_MS = 10 * 60_000;
11
+
12
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
13
+
14
+ interface CacheEntry {
15
+ body: string;
16
+ expiresAt: number;
17
+ }
18
+
19
+ const notesCache = new Map<string, CacheEntry>();
20
+
21
+ function isReleaseVersion(value: string): boolean {
22
+ return value.length <= 64 && VERSION_PATTERN.test(value);
23
+ }
24
+
25
+ function isRecord(value: unknown): value is Record<string, unknown> {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+
29
+ function normalizeReleaseNotes(value: string): string {
30
+ const normalized = value
31
+ .replace(/\r\n?/g, "\n")
32
+ // Keep markdown and whitespace, but never let terminal/control bytes reach the GUI.
33
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
34
+ .trim();
35
+ if (Buffer.byteLength(normalized, "utf8") <= RELEASE_NOTES_MAX_BYTES) return normalized;
36
+
37
+ // The response reader already enforces a byte ceiling. This second guard accounts for the
38
+ // JSON envelope and makes the field safe even when this function is called directly in tests.
39
+ const clipped = Buffer.from(normalized, "utf8")
40
+ .subarray(0, RELEASE_NOTES_MAX_BYTES)
41
+ .toString("utf8")
42
+ .replace(/\uFFFD$/u, "")
43
+ .trimEnd();
44
+ return `${clipped}\n…`;
45
+ }
46
+
47
+ /** Clear the in-memory release-note cache. Intended for focused tests and development reloads. */
48
+ export function clearReleaseNotesCacheForTests(): void {
49
+ notesCache.clear();
50
+ }
51
+
52
+ /**
53
+ * Fetch the exact GitHub Release body for a version.
54
+ *
55
+ * Release notes are display-only metadata. A registry/API failure therefore returns `null` and
56
+ * leaves the npm version check usable. The response is bounded, rendered as text by React, and
57
+ * accepted only when GitHub confirms the requested tag.
58
+ */
59
+ export async function fetchReleaseNotesForVersion(
60
+ version: string,
61
+ fetchFn: typeof fetch = fetch,
62
+ ): Promise<string | null> {
63
+ if (!isReleaseVersion(version)) return null;
64
+
65
+ const now = Date.now();
66
+ const cached = notesCache.get(version);
67
+ if (cached && cached.expiresAt > now) return cached.body;
68
+ if (cached) notesCache.delete(version);
69
+
70
+ const controller = new AbortController();
71
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
72
+ try {
73
+ const response = await fetchFn(`${RELEASE_NOTES_API_URL}/v${encodeURIComponent(version)}`, {
74
+ headers: {
75
+ Accept: "application/vnd.github+json",
76
+ "User-Agent": "Remodex-update-check",
77
+ "X-GitHub-Api-Version": "2022-11-28",
78
+ },
79
+ redirect: "error",
80
+ signal: controller.signal,
81
+ });
82
+ if (!response.ok) return null;
83
+
84
+ const bounded = await readBoundedResponseBody(response, {
85
+ // The JSON envelope and escaping can be larger than the final note body.
86
+ maxBytes: RELEASE_RESPONSE_MAX_BYTES,
87
+ totalTimeoutMs: BODY_TIMEOUT_MS,
88
+ inactivityTimeoutMs: BODY_TIMEOUT_MS,
89
+ });
90
+ if (!bounded.displaySafe || bounded.oversized || bounded.timedOut) return null;
91
+
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(bounded.text);
95
+ } catch {
96
+ return null;
97
+ }
98
+ if (!isRecord(parsed) || parsed.tag_name !== `v${version}` || typeof parsed.body !== "string") {
99
+ return null;
100
+ }
101
+
102
+ const body = normalizeReleaseNotes(parsed.body);
103
+ if (!body) return null;
104
+ notesCache.set(version, { body, expiresAt: Date.now() + CACHE_TTL_MS });
105
+ return body;
106
+ } catch {
107
+ return null;
108
+ } finally {
109
+ clearTimeout(timeout);
110
+ }
111
+ }