@cydm/happy-elves 0.1.0-beta.258 → 0.1.0-beta.259

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.258",
3
+ "version": "0.1.0-beta.259",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.258",
3
+ "version": "0.1.0-beta.259",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.258",
3
+ "version": "0.1.0-beta.259",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import type { RuntimePromptMode, RuntimeTurn } from "../../runtime/dist/index.js";
1
+ import type { RuntimeModelOption, RuntimePromptMode, RuntimeTurn } from "../../runtime/dist/index.js";
2
2
  import type { RuntimePermissionMode } from "./permissions.js";
3
3
  export declare class CodexAppServerSession {
4
4
  private readonly command;
@@ -20,7 +20,12 @@ export declare class CodexAppServerSession {
20
20
  threadId: string | null;
21
21
  constructor(command: string, cwd: string, threadId?: string, onThreadIdChanged?: ((threadId: string) => void) | undefined, restartableEmptyThread?: boolean, initialModel?: string | undefined);
22
22
  ensure(permissionMode?: RuntimePermissionMode, model?: string): Promise<string>;
23
+ discoverRuntimeOptions(): Promise<{
24
+ defaultModel?: string;
25
+ models?: RuntimeModelOption[];
26
+ } | undefined>;
23
27
  private ensureCurrentPermission;
28
+ private loadAllModelListPages;
24
29
  private ensurePermission;
25
30
  private startNewThread;
26
31
  private canRestartEmptyThread;
@@ -56,4 +61,12 @@ export declare class CodexAppServerSession {
56
61
  private noteTurnActivity;
57
62
  private emptyCompletionFailure;
58
63
  }
64
+ export declare function discoverCodexRuntimeOptions(input: {
65
+ command: string;
66
+ cwd: string;
67
+ timeoutMs?: number;
68
+ }): Promise<{
69
+ defaultModel?: string;
70
+ models?: RuntimeModelOption[];
71
+ } | undefined>;
59
72
  //# sourceMappingURL=codex-app-server.d.ts.map
@@ -33,9 +33,43 @@ export class CodexAppServerSession {
33
33
  async ensure(permissionMode, model) {
34
34
  return await this.ensurePermission(normalizeCodexPermissionMode(permissionMode), normalizeModel(model ?? this.initialModel));
35
35
  }
36
+ async discoverRuntimeOptions() {
37
+ if (!this.rpc)
38
+ await this.connect();
39
+ return codexRuntimeOptionsFromModelList(await this.loadAllModelListPages());
40
+ }
36
41
  async ensureCurrentPermission() {
37
42
  return await this.ensurePermission(normalizeCodexPermissionMode(this.appliedPermissionMode), this.appliedModel ?? normalizeModel(this.initialModel));
38
43
  }
44
+ async loadAllModelListPages() {
45
+ const models = [];
46
+ const seenModelIds = new Set();
47
+ const seenCursors = new Set();
48
+ let cursor;
49
+ for (let pageCount = 0; pageCount < CODEX_MODEL_LIST_PAGE_SAFETY_LIMIT; pageCount += 1) {
50
+ const result = await this.request("model/list", {
51
+ limit: CODEX_MODEL_LIST_PAGE_LIMIT,
52
+ ...(cursor !== undefined ? { cursor } : {}),
53
+ });
54
+ for (const model of codexModelsFromModelList(result)) {
55
+ const id = codexModelId(model);
56
+ if (id && seenModelIds.has(id))
57
+ continue;
58
+ if (id)
59
+ seenModelIds.add(id);
60
+ models.push(model);
61
+ }
62
+ const nextCursor = codexModelListNextCursor(result);
63
+ if (nextCursor === undefined)
64
+ return { data: models };
65
+ if (seenCursors.has(nextCursor)) {
66
+ throw new Error(`Codex model/list returned repeated cursor ${JSON.stringify(nextCursor)}`);
67
+ }
68
+ seenCursors.add(nextCursor);
69
+ cursor = nextCursor;
70
+ }
71
+ throw new Error(`Codex model/list exceeded the ${CODEX_MODEL_LIST_PAGE_SAFETY_LIMIT} page safety limit`);
72
+ }
39
73
  async ensurePermission(permissionMode, model) {
40
74
  const hadProcess = Boolean(this.rpc);
41
75
  if (!this.rpc)
@@ -349,6 +383,27 @@ export class CodexAppServerSession {
349
383
  return `Codex turn completed without assistant output after runtime errors${detail ? `: ${detail}` : ""}`;
350
384
  }
351
385
  }
386
+ const CODEX_MODEL_LIST_PAGE_LIMIT = 100;
387
+ const CODEX_MODEL_LIST_PAGE_SAFETY_LIMIT = 100;
388
+ const CODEX_PROVIDER_DISCOVERY_TIMEOUT_MS = 5_000;
389
+ export async function discoverCodexRuntimeOptions(input) {
390
+ const discovery = new CodexAppServerSession(input.command, input.cwd);
391
+ const timeoutMs = Math.max(1, input.timeoutMs ?? CODEX_PROVIDER_DISCOVERY_TIMEOUT_MS);
392
+ let timeout;
393
+ try {
394
+ return await Promise.race([
395
+ discovery.discoverRuntimeOptions(),
396
+ new Promise((_, reject) => {
397
+ timeout = setTimeout(() => reject(new Error(`Codex provider model discovery timed out after ${timeoutMs}ms`)), timeoutMs);
398
+ }),
399
+ ]);
400
+ }
401
+ finally {
402
+ if (timeout)
403
+ clearTimeout(timeout);
404
+ await discovery.close();
405
+ }
406
+ }
352
407
  function codexStderrMessage(text) {
353
408
  try {
354
409
  const parsed = JSON.parse(text);
@@ -415,4 +470,49 @@ function numberField(record, ...keys) {
415
470
  function normalizeModel(value) {
416
471
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
417
472
  }
473
+ function codexModelsFromModelList(value) {
474
+ const record = recordFrom(value);
475
+ return Array.isArray(record.models)
476
+ ? record.models
477
+ : Array.isArray(record.items)
478
+ ? record.items
479
+ : Array.isArray(record.data)
480
+ ? record.data
481
+ : [];
482
+ }
483
+ function codexModelId(value) {
484
+ return stringField(recordFrom(value), "id", "key", "name");
485
+ }
486
+ function codexModelListNextCursor(value) {
487
+ const nextCursor = recordFrom(value).nextCursor;
488
+ if (nextCursor === undefined || nextCursor === null)
489
+ return undefined;
490
+ if (typeof nextCursor !== "string" || nextCursor.length === 0) {
491
+ throw new Error("Codex model/list returned an invalid nextCursor");
492
+ }
493
+ return nextCursor;
494
+ }
495
+ function codexRuntimeOptionsFromModelList(value) {
496
+ const rawModels = codexModelsFromModelList(value);
497
+ const seen = new Set();
498
+ const models = rawModels.flatMap((item) => {
499
+ const model = recordFrom(item);
500
+ const id = codexModelId(model)?.trim();
501
+ if (!id || seen.has(id))
502
+ return [];
503
+ seen.add(id);
504
+ const label = stringField(model, "label", "displayName", "name");
505
+ return [{ id, ...(label ? { label } : {}) }];
506
+ });
507
+ if (models.length === 0)
508
+ return undefined;
509
+ const defaultModel = rawModels
510
+ .map((item) => recordFrom(item))
511
+ .find((item) => item.isDefault === true);
512
+ const defaultModelId = defaultModel ? codexModelId(defaultModel)?.trim() : undefined;
513
+ return {
514
+ ...(defaultModelId ? { defaultModel: defaultModelId } : {}),
515
+ models,
516
+ };
517
+ }
418
518
  //# sourceMappingURL=codex-app-server.js.map
@@ -5,7 +5,11 @@ export type CliRuntimeProviderOptions = {
5
5
  cwd: string;
6
6
  stateDir: string;
7
7
  codexCommand?: string;
8
+ /** Maximum total time for the provider-level Codex catalog probe. */
9
+ codexDiscoveryTimeoutMs?: number;
8
10
  claudeCommand?: string;
9
11
  };
10
- export declare function createCliRuntimeProvider(options: CliRuntimeProviderOptions): HappyElvesRuntime;
12
+ export declare function createCliRuntimeProvider(options: CliRuntimeProviderOptions): HappyElvesRuntime & {
13
+ initialize(): Promise<void>;
14
+ };
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import { readClaudeHistoricalEvents, readClaudeHistoricalSessionPage } from "./claude-history.js";
5
5
  import { renameClaudeSession } from "./claude-rename.js";
6
+ import { discoverCodexRuntimeOptions } from "./codex-app-server.js";
6
7
  import { forkAndTruncateClaudeSession, forkClaudeSession } from "./claude-session-store.js";
7
8
  import { startClaudeTurn } from "./claude-turn.js";
8
9
  import { readCodexCanonicalHistoricalSessionPage, readCodexHistoricalEvents } from "./codex-history.js";
@@ -13,6 +14,7 @@ export { HistoryCursorStaleError, isHistoryCursorStaleError, mergeHistoricalSess
13
14
  export function createCliRuntimeProvider(options) {
14
15
  const codexCommand = options.codexCommand ?? process.env.HAPPY_ELVES_CODEX_COMMAND ?? "codex";
15
16
  const claudeCommand = options.claudeCommand ?? process.env.HAPPY_ELVES_CLAUDE_COMMAND ?? "claude";
17
+ let codexOptions = codexAgentModels();
16
18
  const sessions = new CliSessionStore({ cwd: options.cwd, codexCommand });
17
19
  const historySnapshotDir = path.join(options.stateDir, "history-snapshots");
18
20
  async function listHistoricalSessionPage(input) {
@@ -66,6 +68,24 @@ export function createCliRuntimeProvider(options) {
66
68
  }), loadedByBackendId);
67
69
  }
68
70
  return {
71
+ async initialize() {
72
+ const configured = agentModelsFromEnv("HAPPY_ELVES_CODEX");
73
+ if (configured.models && configured.models.length > 0) {
74
+ codexOptions = configured;
75
+ return;
76
+ }
77
+ const discovered = await discoverCodexRuntimeOptions({
78
+ command: codexCommand,
79
+ cwd: options.cwd,
80
+ timeoutMs: options.codexDiscoveryTimeoutMs,
81
+ }).catch(() => undefined);
82
+ codexOptions = discovered?.models && discovered.models.length > 0
83
+ ? {
84
+ ...(configured.defaultModel ? { defaultModel: configured.defaultModel } : discovered.defaultModel ? { defaultModel: discovered.defaultModel } : {}),
85
+ models: discovered.models,
86
+ }
87
+ : codexAgentModels();
88
+ },
69
89
  getCapabilities() {
70
90
  return {
71
91
  provider: "cli",
@@ -75,7 +95,7 @@ export function createCliRuntimeProvider(options) {
75
95
  },
76
96
  listAgents() {
77
97
  return [
78
- { id: "codex", label: "Codex", ...codexAgentModels() },
98
+ { id: "codex", label: "Codex", ...codexOptions },
79
99
  { id: "claude", label: "Claude Code", ...agentModelsFromEnv("HAPPY_ELVES_CLAUDE") },
80
100
  ];
81
101
  },