Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ In about two minutes, `up`:

`--project` is optional: it clones that repo as the first floor. Leave it out and pick projects in the elevator.

**Signing in the agents.** `--claude-token` uses your Claude subscription; `--anthropic-api-key <key>` uses an API key instead. Leave both out and run `/login` in the first worker's terminal. Codex and OpenCode aren't installed by the script: `deploy/aws.sh ssh` and install them yourself.
**Signing in the agents.** `--claude-token` uses your Claude subscription; `--anthropic-api-key <key>` uses an API key instead. Leave both out and run `/login` in the first worker's terminal. Codex and OpenCode aren't installed by the script: `deploy/aws.sh ssh` and install them yourself. On Windows, the Codex model picker also works when Codex was installed through npm and the Office runs as a background service.

**GitHub.** Your local `gh auth token` is copied to the machine so the office can clone private repos, show the boards and push PRs. Anyone in the office can use it, so pass `--github-token <fine-grained token>` or `--no-github-token` to limit that.

Expand Down
2 changes: 1 addition & 1 deletion docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The Cursor CLI reads hooks from `~/.cursor/hooks.json` and from `.cursor/hooks.j

The entries (`sessionStart`, `beforeSubmitPrompt`, `preToolUse`, `postToolUse`, `postToolUseFailure` and `stop`) report to a loopback endpoint with a per-worker token, and pass on only the chat id, your prompt and the tool's name: never the assistant's messages, tool input or output. A subagent's events are ignored. They answer Cursor with no decision of their own. Cursor has no hook for a permission prompt, so a worker waiting on one shows as *working*, with the tool it wants to run, until you answer it in its terminal. A worker on a machine where Cursor isn't signed in is flagged as needing you, and its terminal is where you log in. The **Model** field takes a Cursor model id, with suggestions from `cursor-agent models` when the CLI is signed in; leave it empty to use your Cursor settings. Cursor has no separate effort flag: a parameterized model takes it in brackets, as in `claude-opus-4-8[effort=high]`. Usage and cost stay in Cursor's terminal and your Cursor account; the office doesn't meter them, and the daily budget stays Claude-only. Written against Cursor CLI 2026.09.08.

Codex uses its interactive CLI with `--no-alt-screen`, preserves native sandbox and approval settings, and resumes through `codex resume <session-id>`. Set it as the default with `--agent codex`. The **Model** field lists the models Codex's own picker shows (from `codex debug models`) and **Effort** the levels the picked model supports; they go on its command line as `--model` and `-c model_reasoning_effort=…`, when it's resumed too (Codex resumes on its current settings, not the session's), and a worker's own model replaces a `--model` in `--agent-args`. Leave either on **Default** to use your Codex settings; `/model` inside its terminal still changes both. The office supplies command hooks through per-process config overrides, without editing your Codex configuration. On first use, open the terminal, complete any login/setup, and review the generated Office commands in `/hooks`. Hooks need your native trust approval before session/status tracking works; the office never bypasses that review. Workers that do not report startup are marked as needing input. Codex tasks use local summaries and do not launch Claude for task naming. Its token metrics come from the root rollout identified by the trusted hook, under `CODEX_HOME/sessions` or `CODEX_HOME/archived_sessions` (default `~/.codex`). The reader checks the session identity, bounds file reads, and sends only normalized counters to the browser. Snapshots update after hooks and on a 10-second poll, survive restarts, and reset for a new session. Cache and reasoning are counted once. Child-session usage is excluded; USD cost and API-call counts are shown as unavailable. The rollout format is version-dependent (verified against 0.154.0); missing or unsupported records remain unavailable instead of being shown as zero. Reads inspect at most a 1 MiB header and the latest 4 MiB of a rollout; if no newer counter is found in that tail, the last known snapshot remains visible.
Codex uses its interactive CLI with `--no-alt-screen`, preserves native sandbox and approval settings, and resumes through `codex resume <session-id>`. Set it as the default with `--agent codex`. The **Model** field lists the models Codex's own picker shows (from `codex debug models`) and **Effort** the levels the picked model supports; they go on its command line as `--model` and `-c model_reasoning_effort=…`, when it's resumed too (Codex resumes on its current settings, not the session's), and a worker's own model replaces a `--model` in `--agent-args`. On Windows, an npm-installed Codex is queried through its Node entry point, so the model list also loads when the Office background service has no Codex desktop binary on its search path. Leave either on **Default** to use your Codex settings; `/model` inside its terminal still changes both. The office supplies command hooks through per-process config overrides, without editing your Codex configuration. On first use, open the terminal, complete any login/setup, and review the generated Office commands in `/hooks`. Hooks need your native trust approval before session/status tracking works; the office never bypasses that review. Workers that do not report startup are marked as needing input. Codex tasks use local summaries and do not launch Claude for task naming. Its token metrics come from the root rollout identified by the trusted hook, under `CODEX_HOME/sessions` or `CODEX_HOME/archived_sessions` (default `~/.codex`). The reader checks the session identity, bounds file reads, and sends only normalized counters to the browser. Snapshots update after hooks and on a 10-second poll, survive restarts, and reset for a new session. Cache and reasoning are counted once. Child-session usage is excluded; USD cost and API-call counts are shown as unavailable. The rollout format is version-dependent (verified against 0.154.0); missing or unsupported records remain unavailable instead of being shown as zero. Reads inspect at most a 1 MiB header and the latest 4 MiB of a rollout; if no newer counter is found in that tail, the last known snapshot remains visible.

OpenCode metrics come from assistant-message token/cost records exposed by its plugin SDK. Updated messages replace prior values so streaming updates do not add the same call twice; existing root and child sessions are loaded on resume. History that is still loading or unavailable is labeled as partial. These snapshots update usage only, so a report cannot dismiss a permission request. The daily budget and historical ledger remain Claude-only.

Expand Down
30 changes: 28 additions & 2 deletions src/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// reasoning efforts it takes, where the CLI says. Each is asked without a shell, and only what
// looks like a model gets through.
import { spawn } from 'node:child_process';
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { AGENT_EFFORTS, isValidCodexModel, isValidCursorModel, isValidGrokModel, isValidOpenCodeModel, type AgentProvider, type ModelOption } from '../shared/providers.js';
Expand Down Expand Up @@ -133,9 +133,35 @@ export async function fetchGrokModels(command: string, cwd: string, runner: Mode
* Run `codex debug models` without a shell: Codex's catalogue as JSON, of which the office takes
* the models its own picker lists, what each is called and the reasoning efforts it supports.
*/
export function resolveCodexModelCommand(
command: string,
platform = process.platform,
searchPath = process.env.PATH ?? '',
exists: (file: string) => boolean = existsSync,
): { file: string; args: string[] } {
if (platform !== 'win32' || !/^codex(?:\.(?:exe|cmd))?$/i.test(path.win32.basename(command)))
return { file: command, args: [] };

const dirs = path.win32.isAbsolute(command)
? [path.win32.dirname(command)]
: searchPath.split(path.win32.delimiter).map((dir) => dir.replace(/^"|"$/g, '')).filter(Boolean);
for (const dir of dirs) {
const executable = path.win32.join(dir, 'codex.exe');
if (exists(executable)) return { file: executable, args: [] };

// npm installs a .cmd shim, which Node cannot spawn with shell:false on Windows. Run its
// adjacent JavaScript entry point with this process's Node instead of opening a shell.
const shim = path.win32.join(dir, 'codex.cmd');
const script = path.win32.join(dir, 'node_modules', '@openai', 'codex', 'bin', 'codex.js');
if (exists(shim) && exists(script)) return { file: process.execPath, args: [script] };
}
return { file: command, args: [] };
}

export async function fetchCodexModels(command: string, cwd: string, runner: ModelCommandRunner = runModelCommand): Promise<ModelOption[]> {
try {
const result = await runner(command, ['debug', 'models'], { cwd, timeout: MODEL_COMMAND_TIMEOUT_MS, maxBuffer: MODEL_DETAILS_MAX_BUFFER });
const invocation = runner === runModelCommand ? resolveCodexModelCommand(command) : { file: command, args: [] };
const result = await runner(invocation.file, [...invocation.args, 'debug', 'models'], { cwd, timeout: MODEL_COMMAND_TIMEOUT_MS, maxBuffer: MODEL_DETAILS_MAX_BUFFER });
const listed: unknown = JSON.parse(result.stdout)?.models;
if (!Array.isArray(listed)) throw new Error('no models');
const models: ModelOption[] = [];
Expand Down
18 changes: 17 additions & 1 deletion tests/models.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { isValidGrokModel, isValidMuseModel, isValidOpenCodeModel } from '../src/shared/providers.js';
import { MODEL_LISTERS, createModelCatalogue, fetchCodexModels, fetchCursorModels, fetchGrokModels, fetchOpenCodeModels, type ModelCommandRunner } from '../src/server/models.js';
import { MODEL_LISTERS, createModelCatalogue, fetchCodexModels, fetchCursorModels, fetchGrokModels, fetchOpenCodeModels, resolveCodexModelCommand, type ModelCommandRunner } from '../src/server/models.js';
import { AGENT_PROVIDERS, PROVIDER_META } from '../src/shared/providers.js';

test('OpenCode model ids require provider/model and reject whitespace or control characters', () => {
Expand Down Expand Up @@ -94,6 +94,22 @@ test('Codex catalogue errors do not expose command output', async () => {
await assert.rejects(fetchCodexModels('codex', '/project', async () => ({ stdout: '{"models":"none"}', stderr: '' })), /unavailable/i);
});

test('Codex model lookup runs an npm-installed Windows CLI without a shell', () => {
const npm = String.raw`C:\Users\test\AppData\Roaming\npm`;
const script = String.raw`C:\Users\test\AppData\Roaming\npm\node_modules\@openai\codex\bin\codex.js`;
const files = new Set([String.raw`C:\Users\test\AppData\Roaming\npm\codex.cmd`, script]);
const exists = (file: string) => files.has(file);
assert.deepEqual(resolveCodexModelCommand('codex', 'win32', `C:\\Tools;${npm}`, exists), { file: process.execPath, args: [script] });
assert.deepEqual(resolveCodexModelCommand(String.raw`C:\Users\test\AppData\Roaming\npm\codex.cmd`, 'win32', '', exists), { file: process.execPath, args: [script] });
assert.deepEqual(resolveCodexModelCommand('codex', 'linux', '', exists), { file: 'codex', args: [] });
});

test('Codex model lookup prefers a native Windows executable', () => {
const executable = String.raw`C:\Tools\codex.exe`;
const exists = (file: string) => file === executable;
assert.deepEqual(resolveCodexModelCommand('codex', 'win32', String.raw`C:\Tools`, exists), { file: executable, args: [] });
});

test('every provider that says its CLI lists its models has a lister, and only those', () => {
const listed = AGENT_PROVIDERS.filter((p) => PROVIDER_META[p].models?.catalog);
assert.deepEqual(Object.keys(MODEL_LISTERS).sort(), [...listed].sort());
Expand Down