nexus-windows-codex 3.1.2 → 3.1.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.1.3] - 2026-10-25 — Dialogs, file choosers, form fields
11
+
12
+ ### Added
13
+ - `windows` also lists open dialogs (a Chrome or Edge "Open" box: owned, without WS_EX_APPWINDOW, absent before) with
14
+ `dialogOf`, their owner; by title the dialog is chosen, by app name the main window comes first.
15
+ - `setup.ps1`: one-step setup for people who never used a terminal (`irm …/setup.ps1 | iex`): installs Node.js LTS
16
+ through winget when Node 22+ is missing, downloads the server from npm and checks that it answers
17
+ (`initialize`, `tools/list`), then registers it in Codex's `config.toml` if it is not there yet; safe to run again.
18
+ - Codex plugin (`.codex-plugin/plugin.json`, `.mcp.json`) and the repository as its own plugin marketplace
19
+ (`codex plugin marketplace add pironjulien/NexusWindows-Codex`).
20
+
21
+ ### Fixed
22
+ - A click on a file chooser trigger ("Choose file", "Browse…", "Upload") is verified only when its dialog opens,
23
+ waited for about 4 s (Edge opens it after more than 1.6 s): before, the click came back "observed" and the dialog
24
+ appeared later; without a dialog the step is `unmet` (`fileChooser: not-opened`), never replayed.
25
+ - A line break typed in a form field was refused because of a nearby help button ("Why can't I send this item?"):
26
+ a neighbour of the field now counts only as a command button (5 words at most, no "?"); a composer's Send button
27
+ still requires confirmation. The refusal names the field.
28
+
29
+ ### Changed
30
+ - README: installation rewritten for beginners (one step, plugin, Codex app settings, `codex mcp add`); `npx` is
31
+ started directly by Codex on Windows, no `cmd /c` needed.
32
+
10
33
  ## [3.1.2] - 2026-10-25 — Docs that match the code
11
34
 
12
35
  ### Fixed
package/README.md CHANGED
@@ -62,18 +62,47 @@ bench.
62
62
 
63
63
  ## Installation
64
64
 
65
- Requirements: Windows 10 or 11 and Node 22+. The npm package carries a self-contained helper: no .NET install.
65
+ Windows 10 or 11 and the [Codex app](https://openai.com/codex/) (or the Codex CLI). The server runs on
66
+ [Node.js](https://nodejs.org/) 22 or later; the npm package carries a self-contained helper: no .NET install.
66
67
 
67
- From [npm](https://www.npmjs.com/package/nexus-windows-codex), in Codex's `~/.codex/config.toml`:
68
+ **One step** (installs Node.js LTS if missing, downloads and checks the server, registers it in Codex): open
69
+ **Windows PowerShell** (Start menu, type `powershell`) and paste:
70
+
71
+ ```powershell
72
+ irm https://raw.githubusercontent.com/pironjulien/NexusWindows-Codex/main/setup.ps1 | iex
73
+ ```
74
+
75
+ Then quit Codex completely and open it again. [`setup.ps1`](https://github.com/pironjulien/NexusWindows-Codex/blob/main/setup.ps1)
76
+ is short and can be read first; running it again only does what is missing. Node.js comes from its official package
77
+ through winget (Windows asks once for approval).
78
+
79
+ **Codex plugin** (the same server; Node.js 22+ required). This repository is a plugin marketplace. In the Codex app:
80
+ Settings → Plugins → Add → Add a marketplace, source `pironjulien/NexusWindows-Codex`, then install
81
+ **NexusWindows** from the Plugins directory. Or in a terminal:
82
+
83
+ ```powershell
84
+ codex plugin marketplace add pironjulien/NexusWindows-Codex
85
+ codex plugin add nexus-windows@nexus-windows-codex
86
+ ```
87
+
88
+ **By hand** (Node.js 22+ required, from the [official installer](https://nodejs.org/)). In the Codex app: Settings →
89
+ Plugins → Add → Add an MCP server, name `nexus-windows`, type STDIO, command `npx`, arguments `-y` and
90
+ `nexus-windows-codex`. Or in a terminal:
91
+
92
+ ```powershell
93
+ codex mcp add nexus-windows -- npx -y nexus-windows-codex
94
+ ```
95
+
96
+ Or in `~/.codex/config.toml` (shared by the Codex app, CLI and IDE extension):
68
97
 
69
98
  ```toml
70
99
  [mcp_servers.nexus-windows]
71
- command = "cmd"
72
- args = ["/c", "npx", "-y", "nexus-windows-codex"]
100
+ command = "npx"
101
+ args = ["-y", "nexus-windows-codex"]
73
102
  ```
74
103
 
75
- On Windows, `npx` is a `.cmd` script, hence `cmd /c`. The same command works in any MCP client
76
- (`"command": "cmd", "args": ["/c", "npx", "-y", "nexus-windows-codex"]`).
104
+ Restart Codex, then ask: "Open Notepad and write a shopping list: milk, eggs, bread." Other MCP clients use the same
105
+ command (`npx -y nexus-windows-codex`; a client that cannot start `.cmd` scripts needs `cmd /c npx -y nexus-windows-codex`).
77
106
 
78
107
  ### Codex skill (alternative, from a clone)
79
108
 
@@ -100,7 +129,7 @@ the JavaScript module cache does not reload.
100
129
 
101
130
  | Tool | Purpose |
102
131
  |---|---|
103
- | `windows(filter)` | Application windows (id, app, title, pid), like the taskbar; password managers never listed |
132
+ | `windows(filter)` | Application windows (id, app, title, pid), like the taskbar, plus open dialogs with `dialogOf` (their owner); password managers never listed |
104
133
  | `look(window, mode, region, marks)` | One window by id or a unique part of its title or app: UI Automation tree, plus a capture that works on covered windows with `mode="image"` or `"both"`; `region` zooms on an area of the last image; `marks` draws tree line numbers on the image; selects the window for `act` |
105
134
  | `act(steps)` | One step, or up to 32 each with its own `expect`, on the window of the last `look`; stops at the first step not verified |
106
135
  | `launch(app)` | Start an application (name, path, Store app id), a folder, a document or a Settings page (`ms-settings:`) and give the foreground back |
@@ -329,6 +358,8 @@ native/ .NET 8 helper: Program.cs (modes --server, --com-worker, --stop
329
358
  ForegroundToken.cs, Win32.cs
330
359
  skill/nexus-windows/ SKILL.md and references (API, per-application notes)
331
360
  tests/ node:test suites; tests/live: real validation fixture (WinForms, Win32, WPF)
361
+ setup.ps1 one-step setup: Node.js if missing, server from npm checked, registered in Codex
362
+ .codex-plugin/, .mcp.json, .agents/plugins/marketplace.json Codex plugin and its marketplace
332
363
  install.ps1 builds the helper and installs the skill
333
364
  test.ps1 offline check; -Full adds the real validation
334
365
  ```
@@ -141,6 +141,14 @@ export function keyKinds(spec) {
141
141
  }).filter(Boolean);
142
142
  }
143
143
 
144
+ // Bouton d'envoi possible d'un champ : Button ou SplitButton au libellé de commande (5 mots au plus, sans « ? »). Un lien
145
+ // d'aide « Pourquoi ne puis-je pas envoyer cet élément ? » à côté de « Description* » d'un formulaire n'est pas ce
146
+ // qu'Entrée déclenche (faux refus vu le 25/10 par NexusWindows for Claude) ; « Envoyer » d'un composeur le reste.
147
+ const commandButton = (e, elements) => {
148
+ if (!['button', 'splitbutton'].includes(e.role)) return false;
149
+ const label = String(labelOf(e, elements)).trim();
150
+ return !label.includes('?') && label.split(/\s+/).filter(Boolean).length <= 5;
151
+ };
144
152
  // Éléments d'action du groupe du champ : sous-arbre de son ancêtre au plus NEIGHBOR_LEVELS au-dessus, jamais la racine.
145
153
  function neighbors(field, elements, policy) {
146
154
  const ancestors = field.ancestors ?? [];
@@ -149,7 +157,7 @@ function neighbors(field, elements, policy) {
149
157
  let found = [];
150
158
  for (let levels = Math.min(NEIGHBOR_LEVELS, ancestors.length - 1); levels > 0 && levels <= ancestors.length - 1; levels++) {
151
159
  const group = ancestors[ancestors.length - levels];
152
- found = elements.filter(e => e !== field && e.ancestors?.includes(group) && liveTrigger(e, policy));
160
+ found = elements.filter(e => e !== field && e.ancestors?.includes(group) && liveTrigger(e, policy) && commandButton(e, elements));
153
161
  if (found.some(e => policy.confirm?.test(plain(labelOf(e, elements))))) break;
154
162
  }
155
163
  return found;
package/lib/mcp.mjs CHANGED
@@ -55,7 +55,7 @@ const WRITE = { readOnlyHint: false, destructiveHint: true, idempotentHint: fals
55
55
  // compte rendu d'étape commence par ✓ vérifié, = déjà fait, ~ issue inconnue (jamais rejouée), ✗ échec ou refus.
56
56
  export const TOOLS = [
57
57
  { name: 'windows', title: 'List windows', annotations: { ...READ, title: 'List windows' },
58
- description: 'Application windows on the desktop (id, app, title, pid), like the taskbar. Password managers are never listed. filter: part of a title or app.',
58
+ description: 'Application windows on the desktop (id, app, title, pid), like the taskbar, plus open dialogs (a file "Open" box…) with dialogOf: the window that owns them; look at a dialog by its id or title. Password managers are never listed. filter: part of a title or app.',
59
59
  inputSchema: { type: 'object', additionalProperties: false, properties: { filter: { type: 'string' } } } },
60
60
  { name: 'look', title: 'Look at a window', annotations: { ...READ, title: 'Look at a window' },
61
61
  description: 'See a window in the background, without bringing it forward: UI Automation tree (roles, names, ids, values, states) and, ' +
@@ -155,7 +155,9 @@ export function regionToScreen(image, region) {
155
155
 
156
156
  function stepLine(step) {
157
157
  const how = [step.technique, step.foregroundTaken ? 'foreground taken' : null].filter(Boolean).join(', ');
158
- return `${mark(step.status)} ${step.kind} — ${step.status}${how ? ` (${how})` : ''}${step.error ? `: ${step.error}` : ''}`;
158
+ const chooser = step.fileChooser === 'opened' ? ': file dialog opened (look at it by its title)'
159
+ : step.fileChooser === 'not-opened' ? ': no file dialog opened (real click sent, effect not seen); look again' : '';
160
+ return `${mark(step.status)} ${step.kind} — ${step.status}${how ? ` (${how})` : ''}${chooser}${step.error ? `: ${step.error}` : ''}`;
159
161
  }
160
162
 
161
163
  export class WindowsTools {
@@ -178,7 +180,15 @@ export class WindowsTools {
178
180
  else {
179
181
  const wanted = String(window).toLowerCase();
180
182
  found = listed.filter(w => w.title.toLowerCase() === wanted);
181
- if (!found.length) found = listed.filter(w => w.title.toLowerCase().includes(wanted) || w.app.toLowerCase().includes(wanted));
183
+ if (!found.length) {
184
+ // Par titre, un dialogue est visé comme une autre fenêtre ; par nom d'application seulement, sa fenêtre principale
185
+ // passe d'abord (la boîte « Ouvrir » de Chrome ne rend pas « chrome » ambigu).
186
+ const byTitle = listed.filter(w => w.title.toLowerCase().includes(wanted));
187
+ const byApp = listed.filter(w => !byTitle.includes(w) && w.app.toLowerCase().includes(wanted));
188
+ const mains = byApp.filter(w => !w.dialogOf);
189
+ found = [...byTitle, ...mains];
190
+ if (!found.length) found = byApp;
191
+ }
182
192
  }
183
193
  if (found.length !== 1) {
184
194
  throw new Error(found.length ? `${found.length} windows match "${window}": ${found.map(w => `${w.id} "${w.title}"`).join(', ')}` : `No window matches "${window}"; list windows`);
@@ -11,8 +11,10 @@ export class NoSkyError extends Error {
11
11
 
12
12
  export function nativeProvider(native) {
13
13
  if (!native?.listWindows) throw new Error('The native helper is required');
14
+ // Dialogue (fenêtre possédée : boîte « Ouvrir » de Chrome) : son propriétaire est imposé au helper à chaque requête.
14
15
  const list = async () => (await native.listWindows(0)).windows.map(w => ({
15
16
  id: w.id, app: w.app, title: w.title ?? '', pid: w.pid, nativeOnly: true,
17
+ ...(w.owner ? { owner: w.owner, dialogOf: { id: w.owner, title: w.ownerTitle ?? '' } } : {}),
16
18
  }));
17
19
  const refuse = what => async () => { throw new NoSkyError(what); };
18
20
  return {
package/lib/session.mjs CHANGED
@@ -90,6 +90,11 @@ const TYPE_CHUNK = 1; // frappe physique par graphème : l'arrêt d'urgence est
90
90
  const PRESENCE_POLL_MS = 50, PRESENCE_POLLS = 33; // ≈ 1,618 s d'attente du retour du garde avant refus
91
91
  // 'key' : touches et raccourcis Ctrl/Maj postés en Win32/WinForms (banc commun du 21/10) ; ailleurs, non applicable → Sky.
92
92
  const BACKGROUND_KINDS = new Set(['click', 'setValue', 'typeText', 'secondary', 'select', 'menu', 'key']);
93
+ // Déclencheur d'un sélecteur de fichiers : « Upload », « Parcourir… » seuls, ou un verbe suivi d'un objet fichier
94
+ // (« Choisir un fichier: Aucun fichier… » de Chromium, « Choose files », « Ajouter des pièces jointes »). La boîte est
95
+ // attendue au plus ≈ 1,618 × 2,618 s : Edge l'a ouverte après plus de 1,6 s le 25/10.
96
+ const FILE_CHOOSER = /^\s*(?:(?:upload|browse|parcourir|t[eé]l[eé]verser)(?:\s+(?:a|the|your|un|une|des|vos))?(?:\s+(?:fichiers?|files?|documents?|images?|photos?))?|(?:choisir|s[eé]lectionner|ajouter|importer|joindre|choose|select|add|attach)(?:\s+(?:un|une|des|le|les|vos|a|the|your))?\s+(?:fichiers?|files?|documents?|images?|photos?|pi[eè]ces?\s+jointes?|attachments?))\s*(?:…|\.\.\.)?\s*(?::|$)/i;
97
+ const CHOOSER_MS = 4236;
93
98
  // Champ destinataire (À, Cc, Cci, To, Destinataire, numéro) : montré à part dans le résumé de l'accord.
94
99
  const RECIPIENT_FIELD = /^((a|to|cc|cci|bcc)( recipients?| destinataires?)?|destinataires?|recipients?|e ?mail|courriel|adresse (e ?mail|electronique)|email address|tel|telephone|numero de telephone|phone( number)?|beneficiaire|iban)( |$)/;
95
100
  const plainName = name => String(name).normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().replace(/[^0-9a-z]+/g, ' ').trim();
@@ -893,6 +898,8 @@ export class WindowsSession {
893
898
  if (matches.length === 1) field = matches[0];
894
899
  }
895
900
  field ??= basis.filter(e => e.focused === true).sort((a, b) => b.depth - a.depth)[0] ?? null;
901
+ // Le refus et le résumé de l'accord nomment le champ (« saut de ligne tapé dans « Description* » »).
902
+ if (spec.kind === 'typeText' && field) what = what.replace(' dans le texte', ` tapé dans « ${String(gardeFous.labelOf(field, basis)).trim().slice(0, 60)} »`);
896
903
  for (const kind of kinds) fail(gardeFous.keyTriggers(basis, field, kind, policy));
897
904
  }
898
905
  if (!outward.length) return null;
@@ -1194,7 +1201,7 @@ export class WindowsSession {
1194
1201
  this.#observation = observation;
1195
1202
  return observation;
1196
1203
  }
1197
- async #settle(spec, coordinate) {
1204
+ async #settle(spec, coordinate, { dialogMs = 0 } = {}) {
1198
1205
  const refresh = spec.refresh ?? (coordinate ? 'both' : 'text');
1199
1206
  // Fenêtre liée fermée par l'action (Alt+F4, bouton Fermer, dialogue qui ferme l'application) : état terminal relu
1200
1207
  // par le helper à toute profondeur de pile, avant chaque capture et après une capture en échec, sans recapturer le
@@ -1244,6 +1251,16 @@ export class WindowsSession {
1244
1251
  let after = await capture(spec.expect && refresh === 'image' ? 'both' : refresh, change);
1245
1252
  if (!after) return closed();
1246
1253
  if (recovered) change = recovered;
1254
+ // Boîte attendue (sélecteur de fichiers) : lectures seules jusqu'à son ouverture ; Edge l'ouvre après plus de 1,6 s.
1255
+ if (dialogMs > 0 && change?.reason !== 'opened') {
1256
+ const deadline = Date.now() + dialogMs;
1257
+ while (Date.now() < deadline && change?.reason !== 'opened') {
1258
+ await sleep(100);
1259
+ const followed = await follow();
1260
+ if (followed === GONE) return closed();
1261
+ if (followed) { change = followed; after = await capture(refresh, change); if (!after) return closed(); }
1262
+ }
1263
+ }
1247
1264
  if (spec.expect && !checkExpectation(after.elements, spec.expect)) {
1248
1265
  const deadline = Date.now() + (spec.timeoutMs ?? 1618);
1249
1266
  while (Date.now() < deadline && !checkExpectation(after.elements, spec.expect)) {
@@ -1350,6 +1367,9 @@ export class WindowsSession {
1350
1367
  let accord;
1351
1368
  try { accord = await this.#confirmation(spec, before, selected, screenshot); }
1352
1369
  catch (error) { if (error.refused) this.#record(spec.kind, error.confirmationRequired ? 'confirmation-required' : 'refused', Date.now() - start); throw error; }
1370
+ // Sélecteur de fichiers (<input type=file> « Choisir un fichier », « Parcourir… », « Upload ») : boîte attendue.
1371
+ const chooser = spec.kind === 'click' && Boolean(selected) && (spec.count ?? 1) === 1 && (spec.button ?? 'left') === 'left' &&
1372
+ FILE_CHOOSER.test(String(gardeFous.labelOf(selected, before.elements)));
1353
1373
  const background = this.#backgroundPlan(spec, before, selected, coordinate, screenshot);
1354
1374
  // Sans technique d'arrière-plan possible, les contrôles de l'entrée physique précèdent toute consommation.
1355
1375
  if (!background) selected = this.#physicalTarget(spec, original, before) ?? selected;
@@ -1382,7 +1402,7 @@ export class WindowsSession {
1382
1402
  }
1383
1403
  }
1384
1404
  if (outcome) {
1385
- ({ after, change } = await this.#settle(spec, coordinate));
1405
+ ({ after, change } = await this.#settle(spec, coordinate, { dialogMs: chooser ? CHOOSER_MS : 0 }));
1386
1406
  // Repli physique seulement après un « none » RELU par le helper : rien n'a pris, rien n'est parti. Après « sent »
1387
1407
  // (transmis sans relecture possible), un état encore inchangé ne prouve rien : l'effet peut être lent (revue du
1388
1408
  // 16/10 : effet arrivé à 2,5 s, après la fenêtre de 1,618 s ; le repli refaisait un vrai clic, double action
@@ -1417,7 +1437,7 @@ export class WindowsSession {
1417
1437
  physical = await this.#runPhysical(spec, before, selected, screenshot);
1418
1438
  outcome = physical.outcome;
1419
1439
  attempts.push({ technique: outcome.technique, mechanism: outcome.mechanism, effect: outcome.effect });
1420
- ({ after, change } = await this.#settle(spec, coordinate));
1440
+ ({ after, change } = await this.#settle(spec, coordinate, { dialogMs: chooser ? CHOOSER_MS : 0 }));
1421
1441
  }
1422
1442
  // Helper du bail perdu pendant l'envoi ou la relecture (fond compris) : l'entrée a pu partir sans jeton ; inconnue
1423
1443
  // dès cette action, jamais « verified » (relecture de Codex, 21/10).
@@ -1440,8 +1460,12 @@ export class WindowsSession {
1440
1460
  this.#record(spec.kind, 'unknown', Date.now() - start, outcome?.technique ?? null, outcome?.foregroundTaken === true);
1441
1461
  throw new Error('Input or refresh outcome is unknown. Observe again before deciding; do not replay automatically.', { cause });
1442
1462
  }
1443
- const verified = Boolean(spec.expect && checkExpectation(after.elements, spec.expect));
1444
- const status = physical?.interrupted ? 'interrupted' : verified ? 'verified' : unconfirmed ? 'sent-unconfirmed' : spec.expect ? 'unmet' : 'observed';
1463
+ // Déclencheur d'un sélecteur de fichiers : le clic n'est attesté que par la boîte ouverte et suivie ; sans elle,
1464
+ // « unmet » (un changement d'image ne prouve rien, vu le 25/10 dans Chrome par NexusWindows for Claude).
1465
+ const chooserOpened = chooser && change?.reason === 'opened';
1466
+ const verified = chooser ? chooserOpened && (!spec.expect || checkExpectation(after.elements, spec.expect))
1467
+ : Boolean(spec.expect && checkExpectation(after.elements, spec.expect));
1468
+ const status = physical?.interrupted ? 'interrupted' : verified ? 'verified' : chooser ? 'unmet' : unconfirmed ? 'sent-unconfirmed' : spec.expect ? 'unmet' : 'observed';
1445
1469
  const result = freeze({
1446
1470
  status, verified: verified && !physical?.interrupted, performed: true, technique: outcome.technique, mechanism: outcome.mechanism,
1447
1471
  foregroundTaken: outcome.foregroundTaken, attempts, julienInput: Boolean(physical?.julienInput),
@@ -1451,6 +1475,7 @@ export class WindowsSession {
1451
1475
  ...(Object.hasOwn(outcome, 'contextMenu') ? { contextMenu: outcome.contextMenu } : {}),
1452
1476
  ...(physical?.interrupted ? { interrupted: physical.interrupted, typed: physical.typed } : {}),
1453
1477
  ...(physical ? { presence: physical.presence ?? 'none', waitedMs: physical.waitedMs ?? 0 } : {}),
1478
+ ...(chooser ? { fileChooser: chooserOpened ? 'opened' : 'not-opened' } : {}),
1454
1479
  windowChange: change, durationMs: Date.now() - start, observation: after,
1455
1480
  });
1456
1481
  this.#record(spec.kind, status, result.durationMs, outcome.technique, outcome.foregroundTaken === true);
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/native/bin/NexusCodex.UIA.exe
3
- size: 35244346 bytes
4
- sha256: 15e9d75e1756c8b0bb63c0bdaad3ee5e18aa1c5a09ab6047686b7326716c44b2
3
+ size: 35244814 bytes
4
+ sha256: 0ea640868ef2c16c6eb9f0b14a8593e29de71912e2c8cc5dc7e4adcd60773ab0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-windows-codex",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "NexusWindows for Codex: Windows computer use in the background for OpenAI Codex (MCP server and Codex skill).",
5
5
  "type": "module",
6
6
  "license": "MIT",