@golba98/codexa 1.0.5 → 1.0.7

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.
package/README.md CHANGED
@@ -26,8 +26,10 @@ codexa --version
26
26
  | Local dev / running from source | [Bun](https://bun.sh) |
27
27
  | Codex/OpenAI route | Codex CLI installed and authenticated |
28
28
  | Claude/Anthropic route | Claude Code CLI installed and authenticated |
29
- | Gemini/Google route | Gemini CLI installed and authenticated |
29
+ | Mistral Vibe route | Mistral Vibe CLI (`vibe`) installed and authenticated |
30
+ | Antigravity route | Antigravity CLI (`agy`) installed and authenticated |
30
31
  | Local route | LM Studio (or any OpenAI-compatible server) |
32
+ | Gemini/Google route | *(Legacy)* Gemini CLI installed and authenticated (falls back to OpenAI route) |
31
33
 
32
34
  ## Installing Codexa
33
35
 
@@ -73,6 +75,8 @@ Codexa and provider CLIs are independent tools with separate version numbers. Up
73
75
  |------|---------|---------------|--------|
74
76
  | Codex CLI | OpenAI coding agent | `codex --version` | `codex update` or reinstall |
75
77
  | Claude Code CLI | Anthropic coding agent | `claude --version` | `npm install -g @anthropic-ai/claude-code@latest` |
78
+ | Mistral Vibe CLI | Mistral coding agent | `vibe --version` | `vibe --setup` or reinstall |
79
+ | Antigravity CLI | Antigravity coding agent | `agy --version` | Reinstall via package manager |
76
80
  | Codexa | TUI wrapper / workspace experience | `codexa --version` | `npm install -g @golba98/codexa@latest` |
77
81
 
78
82
  ## Provider CLI Setup
@@ -146,9 +150,12 @@ claude
146
150
  npm install -g @anthropic-ai/claude-code@latest
147
151
  ```
148
152
 
149
- ### Installing Gemini CLI
153
+ ### Installing Gemini CLI (Legacy / Deprecated)
150
154
 
151
- Codexa routes to the `gemini` executable. Install the Gemini CLI separately and authenticate per Google's instructions. Once installed and authenticated, Codexa can use it without additional configuration.
155
+ > [!NOTE]
156
+ > Google/Gemini is no longer supported as an active provider route inside Codexa and falls back automatically to the OpenAI/Codex route.
157
+
158
+ If you are using legacy features that route to the `gemini` executable, install the Gemini CLI separately and authenticate per Google's instructions.
152
159
 
153
160
  If the Gemini executable is not on PATH or has a non-standard name, specify it explicitly:
154
161
 
@@ -160,6 +167,30 @@ export GEMINI_EXECUTABLE="/path/to/gemini"
160
167
  gemini_command_path = "/path/to/gemini"
161
168
  ```
162
169
 
170
+ ### Installing Mistral Vibe CLI
171
+
172
+ Codexa supports routing through the Mistral Vibe CLI (`vibe`).
173
+
174
+ 1. **Install Vibe CLI** — Install the Mistral Vibe CLI tool.
175
+ 2. **Authenticate Vibe** — Run `vibe` or `vibe --setup` in a terminal and sign in when prompted.
176
+ 3. **Environment & Config (Optional)**:
177
+ - Codexa automatically detects the active model from your environment (`VIBE_ACTIVE_MODEL`) or configuration files (`.vibe/config.toml` in your project/workspace, or `~/.vibe/config.toml`).
178
+ - You can customize the active session location using the `VIBE_HOME` environment variable.
179
+
180
+ ### Installing Antigravity CLI
181
+
182
+ Codexa supports routing through the Antigravity CLI (`agy`).
183
+
184
+ 1. **Install Antigravity CLI** — Install the Antigravity CLI tool (`agy`).
185
+ 2. **Authenticate Antigravity** — Authenticate the CLI according to your provider setup.
186
+ 3. **Environment & Config (Optional)**:
187
+ - Specifying the `agy` command path:
188
+ ```toml
189
+ # in .codex/config.toml
190
+ antigravity_command_path = "/path/to/agy"
191
+ ```
192
+ - Alternatively, set the `AGY_EXECUTABLE` environment variable.
193
+
163
194
  ### Using Local Models (LM Studio)
164
195
 
165
196
  Codexa can route the Local provider through any OpenAI-compatible server.
@@ -178,7 +209,15 @@ export CODEXA_LOCAL_API_KEY="lm-studio"
178
209
  export CODEXA_LOCAL_MODEL="google/gemma-4-26b-a4b"
179
210
  ```
180
211
 
181
- Workspace provider config in `.codexa/providers.json`:
212
+ Provider configuration is stored in Codexa user data and remains isolated per workspace:
213
+
214
+ - Windows: `%LOCALAPPDATA%\\Codexa\\workspaces\\<workspace-hash>\\providers.json`
215
+ - macOS: `~/Library/Application Support/Codexa/workspaces/<workspace-hash>/providers.json`
216
+ - Linux: `$XDG_DATA_HOME/codexa/workspaces/<workspace-hash>/providers.json` (or `~/.local/share/codexa/...`)
217
+
218
+ Existing `.codexa/providers.json` files are read as a legacy fallback and are never deleted. New saves go to user data, so opening or configuring Codexa no longer adds `.codexa` files to projects.
219
+
220
+ The provider config format is:
182
221
 
183
222
  ```json
184
223
  {
@@ -305,6 +344,7 @@ Type `/` in the composer to access in-app commands. Key commands:
305
344
  | `/verbose` | Toggle verbose output |
306
345
  | `/copy` | Copy last response to clipboard |
307
346
  | `/diagnose github` | Run GitHub connectivity diagnostics |
347
+ | `/diagnose providers` | Collect diagnostics for all configured providers |
308
348
  | `!<command>` | Run a shell command inline |
309
349
 
310
350
  ## Layered Config
@@ -425,6 +465,12 @@ UI-only preferences are stored in `~/.codexa-settings.json` (separate from runti
425
465
 
426
466
  > **Gotcha:** When testing local changes, confirm you are running the local build and not the global one. Use `Get-Command codexa` (Windows) or `which -a codexa` (Linux/macOS) to check.
427
467
 
468
+ ## Maintainer Documentation
469
+
470
+ - [Architecture](docs/ARCHITECTURE.md) explains Codexa's entry points, subsystem boundaries, prompt and rendering flows, provider layers, state model, persistence, and maintenance invariants with diagrams.
471
+ - [Source Guide](docs/SOURCE_GUIDE.md) catalogs the purpose of every file under `src/` and records the maintenance rules for each source area.
472
+ - [Developer Scripts](scripts/README.md) lists every package command and explains launcher, shim, audit, build-metadata, smoke, and test behavior.
473
+
428
474
  ## Local Development
429
475
 
430
476
  **Requirement:** [Bun](https://bun.sh) installed.
@@ -486,6 +532,63 @@ This replaces the global `codexa` with a symlink to the repo. Undo with `npm unl
486
532
  npm install -g .
487
533
  ```
488
534
 
535
+ ## Maintainer release and NPM publishing
536
+
537
+ The package name is `@golba98/codexa`. Publish from the repository root, and
538
+ increment `version` in `package.json` before each release. NPM versions are
539
+ immutable, so never reuse a version that has already been published.
540
+
541
+ Check the current local and registry versions:
542
+
543
+ ```bash
544
+ npm pkg get name version
545
+ npm whoami
546
+ npm view @golba98/codexa version
547
+ npm view @golba98/codexa dist-tags --json
548
+ ```
549
+
550
+ Run the release checks:
551
+
552
+ ```bash
553
+ bun install
554
+ bun run typecheck
555
+ bun test
556
+ git diff --check
557
+ ```
558
+
559
+ Inspect exactly what will be included in the package:
560
+
561
+ ```bash
562
+ npm pack --dry-run
563
+ ```
564
+
565
+ Publish the next version publicly:
566
+
567
+ ```bash
568
+ npm publish --access public
569
+ ```
570
+
571
+ `npm publish` automatically runs `prepublishOnly`, which regenerates build
572
+ metadata, typechecks, and runs the full test suite. Verify the registry and
573
+ the installed command after publishing:
574
+
575
+ ```bash
576
+ npm view @golba98/codexa version
577
+ npm view @golba98/codexa dist-tags --json
578
+ npm install -g @golba98/codexa@latest
579
+ codexa --version
580
+ ```
581
+
582
+ If the release is tracked in Git, commit the version and generated build-info
583
+ change, then tag and push it:
584
+
585
+ ```bash
586
+ git add package.json src/config/buildInfo.ts README.md scripts/README.md
587
+ git commit -m "release: publish Codexa vX.Y.Z"
588
+ git tag vX.Y.Z
589
+ git push origin main --follow-tags
590
+ ```
591
+
489
592
  ## Testing
490
593
 
491
594
  ```bash
@@ -577,7 +680,7 @@ npm install -g @golba98/codexa@latest
577
680
 
578
681
  ### Update notice does not appear
579
682
 
580
- Codexa checks for updates on startup and caches the result for 6 hours (stored in `~/.codexa-update-check.json`). The update notice only appears when the npm registry `latest` version is newer than the running version.
683
+ Codexa checks npm for updates on every interactive startup. The local cache at `~/.codexa-update-check.json` is only used as a best-effort fallback if npm is temporarily unavailable. The update notice appears when the npm registry `latest` version is newer than the running version.
581
684
 
582
685
  Published npm versions are immutable. Versions before the fixed update checker may not show update notices even when a newer package exists. If in doubt, update directly:
583
686
 
@@ -588,9 +691,8 @@ npm install -g @golba98/codexa@latest
588
691
  If you expect a notice but don't see one:
589
692
 
590
693
  1. Check the current registry state: `npm view @golba98/codexa dist-tags --json`
591
- 2. Clear the cache to force a fresh check: delete `~/.codexa-update-check.json`, then restart Codexa
592
- 3. Force an explicit check in-app: `/update check`
593
- 4. Update checks are disabled for local dev builds (`codexa-dev` / `cxd`)
694
+ 2. Force an explicit check in-app: `/update check`
695
+ 3. Update checks are disabled for local dev builds (`codexa-dev` / `cxd`)
594
696
 
595
697
  | Symptom | Fix |
596
698
  |---------|-----|
@@ -606,11 +708,14 @@ If you expect a notice but don't see one:
606
708
 
607
709
  See [CHANGELOG.md](CHANGELOG.md) for the full release history.
608
710
 
609
- **Current release: v1.0.4**
711
+ **Current release: v1.0.7**
610
712
 
611
- v1.0.4 is an update-notice reliability fix:
612
- - Older installed versions can detect when npm `latest` is newer
613
- - `/update check` bypasses stale cache and reports a useful status
614
- - Failed registry lookups do not get cached as “up to date”
713
+ v1.0.7 keeps workspaces clean: provider choices, imported attachments, and default diagnostic logs now live in Codexa user data instead of a project-local `.codexa` directory.
615
714
 
616
- > Versions before the fixed update checker may not show update notices. Update directly with `npm install -g @golba98/codexa@latest`.
715
+ Other recent releases introduce major additions, including:
716
+ - **Mistral Vibe CLI Routing**: Connect through the `vibe` CLI tool (`vibe --setup`).
717
+ - **Antigravity CLI Routing**: Connect through the `agy` CLI tool (`agy`).
718
+ - **Dynamic Model Discovery & Caching**: Models are discovered dynamically and cached locally at `~/.codexa-model-cache.json` for instant launch, with instant provider sync for the model picker.
719
+ - **Native Terminal Scrollback**: History follows native scrollback with composer anchored at the bottom.
720
+ - **Kitty Keyboard Protocol Fix**: Corrected probe output leak.
721
+ - **Google/Gemini Route Deprecation**: In-Codexa routing for Google/Gemini now falls back to OpenAI/Codex.
package/bin/codexa.js CHANGED
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "child_process";
4
- import { appendFileSync, mkdirSync, readFileSync } from "fs";
4
+ import { appendFileSync, mkdirSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
5
6
  import { dirname, join } from "path";
6
7
  import { fileURLToPath } from "url";
7
8
 
8
- process.title = "CODEXA";
9
+ process.title = "CODEXA";
10
+
11
+ function resolveLauncherDebugDir() {
12
+ const dataDir = process.env.CODEXA_DATA_DIR?.trim()
13
+ || (process.platform === "win32"
14
+ ? join(process.env.LOCALAPPDATA?.trim() || process.env.APPDATA?.trim() || join(homedir(), "AppData", "Local"), "Codexa")
15
+ : process.platform === "darwin"
16
+ ? join(homedir(), "Library", "Application Support", "Codexa")
17
+ : join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "codexa"));
18
+ return join(dataDir, "debug");
19
+ }
9
20
 
10
21
  const currentFile = fileURLToPath(import.meta.url);
11
22
  const packageRoot = dirname(dirname(currentFile));
@@ -21,7 +32,7 @@ function writeRenderDebugRecord(kind, fields) {
21
32
  }
22
33
 
23
34
  try {
24
- const debugDir = join(workspaceRoot, ".codexa", "debug");
35
+ const debugDir = resolveLauncherDebugDir();
25
36
  mkdirSync(debugDir, { recursive: true });
26
37
  appendFileSync(
27
38
  process.env.CODEXA_RENDER_DEBUG_FILE?.trim() || join(debugDir, "render-status.log"),
@@ -56,7 +67,7 @@ function writeTerminalTitleDebugRecord(fields) {
56
67
  }
57
68
 
58
69
  try {
59
- const debugDir = join(workspaceRoot, ".codexa", "debug");
70
+ const debugDir = resolveLauncherDebugDir();
60
71
  mkdirSync(debugDir, { recursive: true });
61
72
  appendFileSync(
62
73
  process.env.CODEXA_TERMINAL_TITLE_DEBUG_FILE?.trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golba98/codexa",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Terminal UI for Codexa — TypeScript + Bun + Ink",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.tsx CHANGED
@@ -154,7 +154,8 @@ import {
154
154
  type ModelContextMetadata,
155
155
  } from "./core/providerRuntime/contextMetadata.js";
156
156
  import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspace/workspaceActivity.js";
157
- import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
157
+ import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
158
+ import { resolveCodexaAttachmentDir } from "./core/workspace/appData.js";
158
159
  import {
159
160
  importExternalFile,
160
161
  isImageFile,
@@ -270,9 +271,10 @@ import { AppShell } from "./ui/chrome/AppShell.js";
270
271
  import { TranscriptShell } from "./ui/timeline/TranscriptShell.js";
271
272
  import type { RuntimeAvailability } from "./ui/chrome/RuntimeStatusBar.js";
272
273
  import { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/version/updateCheck.js";
274
+ import { detectGlobalPackageManager, getUpdateCommand } from "./core/version/packageManager.js";
273
275
  import { isLocalDevChannel } from "./core/version/channel.js";
274
276
  import {
275
- isCacheValid,
277
+ isCacheForRunningVersion,
276
278
  loadUpdateCheckCache,
277
279
  saveUpdateCheckCache,
278
280
  } from "./config/updateCheckCache.js";
@@ -394,9 +396,6 @@ export function App({ launchArgs }: AppProps) {
394
396
  ? projectInstructionsLoad.instructions
395
397
  : null;
396
398
  const initialSettings = useRef(loadSettings());
397
- const skippedUpdateVersionRef = useRef<string | null>(
398
- initialSettings.current.updateCheck.skippedUpdateVersion ?? null,
399
- );
400
399
  const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
401
400
  const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
402
401
  if (initialLayeredConfig.current === null) {
@@ -550,6 +549,9 @@ export function App({ launchArgs }: AppProps) {
550
549
  const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
551
550
  const [initialRevisionText, setInitialRevisionText] = useState("");
552
551
  const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResult | null>(null);
552
+ const [startupUpdateDismissed, setStartupUpdateDismissed] = useState(false);
553
+ // Launcher path is fixed for the process lifetime, so detect once.
554
+ const globalPackageManager = useMemo(() => detectGlobalPackageManager(), []);
553
555
  // Transcript mode leaves mouse reporting off so wheel/trackpad input scrolls
554
556
  // the terminal emulator's native scrollback instead of an in-app viewport.
555
557
  const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
@@ -1625,54 +1627,53 @@ export function App({ launchArgs }: AppProps) {
1625
1627
  // eslint-disable-next-line react-hooks/exhaustive-deps
1626
1628
  }, [workspaceRoot]);
1627
1629
 
1628
- // Non-blocking background update check — runs once at startup.
1630
+ // Non-blocking background update check — fetches npm on every interactive startup.
1629
1631
  useEffect(() => {
1630
1632
  const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
1631
1633
  if (!shouldRunStartupUpdateCheck(process.env, ucSettings.enabled)) return;
1632
1634
 
1633
- const timer = setTimeout(() => {
1634
- void (async () => {
1635
- try {
1636
- const cache = loadUpdateCheckCache();
1637
- if (cache && isCacheValid(cache, ucSettings.intervalHours, APP_VERSION)) {
1638
- if (cache.updateAvailable && cache.latestVersion) {
1639
- setUpdateCheckResult({
1640
- status: "update-available",
1641
- currentVersion: cache.currentVersion,
1642
- latestVersion: cache.latestVersion,
1643
- checkedAt: cache.lastChecked,
1644
- });
1645
- if (cache.latestVersion !== skippedUpdateVersionRef.current) {
1646
- setScreen("update-prompt");
1647
- }
1648
- }
1649
- return;
1650
- }
1651
- const result = await checkForUpdates({ enabled: true });
1652
- if (result.status !== "error") {
1653
- setUpdateCheckResult(result);
1654
- saveUpdateCheckCache({
1655
- lastChecked: result.checkedAt,
1656
- currentVersion: result.currentVersion,
1657
- latestVersion: result.latestVersion,
1658
- updateAvailable: result.status === "update-available",
1659
- });
1660
- if (result.status === "update-available" && result.latestVersion) {
1661
- if (result.latestVersion !== skippedUpdateVersionRef.current) {
1662
- setScreen("update-prompt");
1663
- }
1664
- }
1665
- }
1666
- } catch {
1667
- // Never crash the TUI on a failed update check.
1668
- }
1669
- })();
1670
- }, 2000);
1671
-
1672
- return () => clearTimeout(timer);
1635
+ void (async () => {
1636
+ const cache = loadUpdateCheckCache();
1637
+ try {
1638
+ const result = await checkForUpdates({ enabled: true });
1639
+ if (result.status === "error") {
1640
+ // A previously confirmed update is still useful when npm is briefly
1641
+ // unreachable, but it must never suppress the next fresh startup check.
1642
+ if (cache?.updateAvailable && cache.latestVersion && isCacheForRunningVersion(cache, APP_VERSION)) {
1643
+ setUpdateCheckResult({
1644
+ status: "update-available",
1645
+ currentVersion: cache.currentVersion,
1646
+ latestVersion: cache.latestVersion,
1647
+ checkedAt: cache.lastChecked,
1648
+ source: "cache",
1649
+ });
1650
+ }
1651
+ return;
1652
+ }
1653
+
1654
+ setUpdateCheckResult(result);
1655
+ saveUpdateCheckCache({
1656
+ lastChecked: result.checkedAt,
1657
+ currentVersion: result.currentVersion,
1658
+ latestVersion: result.latestVersion,
1659
+ updateAvailable: result.status === "update-available",
1660
+ });
1661
+ } catch {
1662
+ // Never crash the TUI on a failed update check.
1663
+ }
1664
+ })();
1673
1665
  // eslint-disable-next-line react-hooks/exhaustive-deps
1674
1666
  }, []);
1675
1667
 
1668
+ // A startup update prompt must not interrupt an active run or another panel.
1669
+ // Keep the result until the user returns to the idle main screen instead.
1670
+ useEffect(() => {
1671
+ if (startupUpdateDismissed || busy || screen !== "main") return;
1672
+ if (updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion) {
1673
+ setScreen("update-prompt");
1674
+ }
1675
+ }, [busy, screen, startupUpdateDismissed, updateCheckResult]);
1676
+
1676
1677
  // Track clear epoch to suppress stale command result events
1677
1678
  useEffect(() => {
1678
1679
  clearEpochRef.current = sessionState.clearEpoch;
@@ -2160,32 +2161,10 @@ export function App({ launchArgs }: AppProps) {
2160
2161
  }, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
2161
2162
 
2162
2163
  const handleSkipUpdateForSession = useCallback(() => {
2164
+ setStartupUpdateDismissed(true);
2163
2165
  setScreen("main");
2164
2166
  }, []);
2165
2167
 
2166
- const handleSkipUpdateVersion = useCallback((version: string) => {
2167
- initialSettings.current.updateCheck = {
2168
- ...initialSettings.current.updateCheck,
2169
- skippedUpdateVersion: version,
2170
- };
2171
- saveSettings({
2172
- ui: {
2173
- layoutStyle: initialSettings.current.ui.layoutStyle,
2174
- theme: themeSelection.committedTheme,
2175
- workspaceDisplayMode,
2176
- terminalTitleMode,
2177
- showBusyLoader,
2178
- terminalMouseMode,
2179
- customTheme,
2180
- },
2181
- auth: { preference: authPreference },
2182
- header: headerConfig,
2183
- updateCheck: initialSettings.current.updateCheck,
2184
- });
2185
- skippedUpdateVersionRef.current = version;
2186
- setScreen("main");
2187
- }, [authPreference, customTheme, headerConfig, showBusyLoader, terminalMouseMode, terminalTitleMode, themeSelection.committedTheme, workspaceDisplayMode]);
2188
-
2189
2168
  const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
2190
2169
  const gate = guardConfigMutation("mode", busy);
2191
2170
  if (!gate.allowed) {
@@ -3852,13 +3831,12 @@ export function App({ launchArgs }: AppProps) {
3852
3831
 
3853
3832
  const handleImportConfirm = useCallback(async () => {
3854
3833
  if (!pendingImport) return;
3855
- const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
3834
+ const replacements: Array<{ rawPath: string; replacementPath: string }> = [];
3856
3835
  for (const file of pendingImport.files) {
3857
3836
  try {
3858
3837
  const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
3859
3838
  if (destPath) {
3860
- const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
3861
- replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
3839
+ replacements.push({ rawPath: file.rawPath, replacementPath: destPath });
3862
3840
  }
3863
3841
  } catch (err: any) {
3864
3842
  appendErrorEvent("Import failed", `Could not import ${path.basename(file.srcPath)}: ${err.message}`);
@@ -4435,7 +4413,10 @@ export function App({ launchArgs }: AppProps) {
4435
4413
  if (freshResult?.status === "update-available" && freshResult.latestVersion) {
4436
4414
  setScreen("update-prompt");
4437
4415
  } else {
4438
- appendSystemEvent("Update", formatUpdateInstructions(freshResult));
4416
+ appendSystemEvent(
4417
+ "Update",
4418
+ formatUpdateInstructions(freshResult, getUpdateCommand(globalPackageManager).displayCommand),
4419
+ );
4439
4420
  }
4440
4421
  })();
4441
4422
  return;
@@ -4489,9 +4470,7 @@ export function App({ launchArgs }: AppProps) {
4489
4470
 
4490
4471
  if (outsideViolations.length > 0) {
4491
4472
  if (runtimeConfig.policy.allowExternalFileImport) {
4492
- const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
4493
- ? runtimeConfig.policy.attachmentDir
4494
- : path.join(workspaceRoot, runtimeConfig.policy.attachmentDir);
4473
+ const attachmentsDir = resolveCodexaAttachmentDir(workspaceRoot, runtimeConfig.policy.attachmentDir);
4495
4474
  const importFiles: PendingImportFile[] = outsideViolations.map((v) => ({
4496
4475
  srcPath: v.normalizedPath,
4497
4476
  rawPath: v.rawPath,
@@ -4527,6 +4506,7 @@ export function App({ launchArgs }: AppProps) {
4527
4506
  dispatchSession,
4528
4507
  findUserPromptForTurn,
4529
4508
  focusManager,
4509
+ globalPackageManager,
4530
4510
  handleCopy,
4531
4511
  handleClear,
4532
4512
  handleQuit,
@@ -4751,8 +4731,12 @@ export function App({ launchArgs }: AppProps) {
4751
4731
  clearCount={sessionState.clearCount}
4752
4732
  headerConfig={effectiveHeaderConfig}
4753
4733
  updateAvailable={
4754
- updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4755
- ? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
4734
+ screen !== "update-prompt" && updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4735
+ ? {
4736
+ latestVersion: updateCheckResult.latestVersion,
4737
+ currentVersion: updateCheckResult.currentVersion,
4738
+ updateCommand: getUpdateCommand(globalPackageManager).displayCommand,
4739
+ }
4756
4740
  : null
4757
4741
  }
4758
4742
  panel={
@@ -5010,9 +4994,9 @@ export function App({ launchArgs }: AppProps) {
5010
4994
  focusId={FOCUS_IDS.updatePrompt}
5011
4995
  currentVersion={updateCheckResult.currentVersion}
5012
4996
  latestVersion={updateCheckResult.latestVersion}
5013
- onSkip={handleSkipUpdateForSession}
5014
- onSkipUntilNextVersion={handleSkipUpdateVersion}
5015
- />
4997
+ packageManager={globalPackageManager}
4998
+ onSkip={handleSkipUpdateForSession}
4999
+ />
5016
5000
  )}
5017
5001
  </>
5018
5002
  }
@@ -391,12 +391,12 @@ function buildHelpMessage(context: CommandContext): string {
391
391
  ` Current reasoning: ${formatReasoningLabel(context.runtime.reasoningLevel)}`,
392
392
  ` Current plan mode: ${context.runtime.planMode ? "Enabled" : "Disabled"}`,
393
393
  " /copy Copy last response to clipboard",
394
- " /update [check] Show update status and instructions for updating Codexa",
394
+ " /update [status] Check for updates and install the latest Codexa (status: cached result only)",
395
395
  " /help Show this help",
396
- "",
397
- "Local development:",
398
- " npm run install:dev-bin Install the codexa-dev command",
399
- " codexa-dev Run this repo without replacing codexa",
396
+ "",
397
+ "Local development:",
398
+ " npm run install:dev-bin Install the codexa-dev command",
399
+ " codexa-dev Run this repo without replacing codexa",
400
400
  "",
401
401
  "Shortcuts:",
402
402
  " Ctrl+B Open backend picker",
@@ -515,11 +515,11 @@ export function handleCommand(text: string, context: CommandContext): CommandRes
515
515
  message: `Unknown reasoning level for ${context.runtime.model}: ${arg}. Valid: ${valid}`,
516
516
  };
517
517
  }
518
- return {
519
- action: "reasoning",
520
- value: normalized,
521
- message: `Reasoning level switched to ${formatReasoningLabel(normalized)} (runtime metadata unavailable; unverified until runtime)`,
522
- };
518
+ return {
519
+ action: "reasoning",
520
+ value: normalized,
521
+ message: `Reasoning level switched to ${formatReasoningLabel(normalized)} (runtime metadata unavailable; unverified until runtime)`,
522
+ };
523
523
  }
524
524
 
525
525
  case "plan": {
@@ -846,7 +846,9 @@ export function handleCommand(text: string, context: CommandContext): CommandRes
846
846
  return { action: "help", message: buildHelpMessage(context) };
847
847
 
848
848
  case "update":
849
- return { action: "update", value: normalizedArg || "status" };
849
+ // Bare /update forces a fresh registry check; /update status is the
850
+ // explicit no-network path that reuses the cached/in-memory result.
851
+ return { action: "update", value: normalizedArg || "check" };
850
852
 
851
853
  default:
852
854
  return {
@@ -0,0 +1,65 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { APP_VERSION as BUILD_INFO_VERSION } from "./buildInfo.js";
5
+
6
+ // Leaf module: must not import settings.ts or anything under src/core/version
7
+ // (settings.ts re-exports APP_VERSION from here, so that would be a cycle).
8
+
9
+ const CODEXA_PACKAGE_NAME = "@golba98/codexa";
10
+ const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
11
+
12
+ function readPackageVersion(packageJsonPath: string, requireName?: string): string | null {
13
+ try {
14
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
15
+ name?: unknown;
16
+ version?: unknown;
17
+ };
18
+ if (requireName !== undefined && parsed.name !== requireName) return null;
19
+ if (typeof parsed.version !== "string") return null;
20
+ const version = parsed.version.trim();
21
+ return SEMVER_RE.test(version) ? version : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Resolves the version of the running Codexa install:
29
+ * 1. `CODEXA_PACKAGE_ROOT` (set by bin/codexa.js) → that package.json's version
30
+ * 2. Walk up from this module for a package.json named "@golba98/codexa" (local dev)
31
+ * 3. The committed buildInfo.ts APP_VERSION as the last resort
32
+ *
33
+ * `startDir` overrides the walk-up starting directory (test seam).
34
+ */
35
+ export function resolveAppVersion(
36
+ env: NodeJS.ProcessEnv = process.env,
37
+ startDir?: string,
38
+ ): string {
39
+ const packageRoot = env.CODEXA_PACKAGE_ROOT?.trim();
40
+ if (packageRoot) {
41
+ const version = readPackageVersion(join(packageRoot, "package.json"));
42
+ if (version) return version;
43
+ }
44
+
45
+ let dir = startDir ?? dirname(fileURLToPath(import.meta.url));
46
+ for (;;) {
47
+ const version = readPackageVersion(join(dir, "package.json"), CODEXA_PACKAGE_NAME);
48
+ if (version) return version;
49
+ const parent = dirname(dir);
50
+ if (parent === dir) break;
51
+ dir = parent;
52
+ }
53
+
54
+ return BUILD_INFO_VERSION;
55
+ }
56
+
57
+ let cachedVersion: string | null = null;
58
+
59
+ /** Cached form of resolveAppVersion() — the installed version cannot change mid-process. */
60
+ export function getAppVersion(): string {
61
+ if (cachedVersion === null) {
62
+ cachedVersion = resolveAppVersion();
63
+ }
64
+ return cachedVersion;
65
+ }
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
- export const BUILD_COMMIT = "aaffb655176a0acb1bc7800ddbb3e6402e5933f4" as const;
3
- export const APP_VERSION = "1.0.5" as const;
2
+ export const BUILD_COMMIT = "c04f84f315a4b2c4b8bea27d41ac370a27467572" as const;
3
+ export const APP_VERSION = "1.0.7" as const;
@@ -130,7 +130,7 @@ export const DEFAULT_RUNTIME_POLICY: RuntimePolicyConfig = {
130
130
  serviceTier: "flex",
131
131
  personality: "none",
132
132
  allowExternalFileImport: true,
133
- attachmentDir: ".codexa/attachments",
133
+ attachmentDir: "attachments",
134
134
  };
135
135
 
136
136
  export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
@@ -9,7 +9,11 @@ function smartJoin(base: string, ...parts: string[]): string {
9
9
  return isWindowsStylePath(base) ? win32.join(base, ...parts) : join(base, ...parts);
10
10
  }
11
11
 
12
- export { APP_VERSION } from "./buildInfo.js";
12
+ import { getAppVersion } from "./appVersion.js";
13
+
14
+ // Authoritative runtime version: resolved from the installed package.json at
15
+ // startup (buildInfo.ts is only the committed fallback and can drift).
16
+ export const APP_VERSION: string = getAppVersion();
13
17
  export const APP_NAME = "Codexa";
14
18
  export const DEFAULT_BACKEND = "codex-subprocess";
15
19
  export const DEFAULT_MODEL = "gpt-5.4";