@cydm/happy-elves 0.1.0-beta.254 → 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.254",
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.254",
3
+ "version": "0.1.0-beta.259",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -21,17 +21,47 @@ function primitiveSupport(value) {
21
21
  function historyModel(value) {
22
22
  return value === "append-only" || value === "authoritative-linear" ? value : undefined;
23
23
  }
24
+ function stringField(value) {
25
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
26
+ }
27
+ function agentModelOptions(value) {
28
+ if (!Array.isArray(value))
29
+ return undefined;
30
+ const seen = new Set();
31
+ const models = value.flatMap((item) => {
32
+ if (!isRecord(item))
33
+ return [];
34
+ const id = stringField(item.id);
35
+ if (!id || seen.has(id))
36
+ return [];
37
+ seen.add(id);
38
+ const label = stringField(item.label);
39
+ const reason = stringField(item.reason);
40
+ return [{
41
+ id,
42
+ ...(label ? { label } : {}),
43
+ ...(item.unavailable === true ? { unavailable: true } : {}),
44
+ ...(reason ? { reason } : {}),
45
+ }];
46
+ });
47
+ return models.length > 0 ? models : undefined;
48
+ }
24
49
  export function normalizeMachineCapabilities(value) {
25
50
  const record = isRecord(value) ? value : {};
26
51
  const agents = Array.isArray(record.agents)
27
52
  ? record.agents.flatMap((agent) => {
28
53
  if (!isRecord(agent) || typeof agent.id !== "string" || typeof agent.label !== "string")
29
54
  return [];
55
+ const agentHistoryModel = historyModel(agent.historyModel);
56
+ const defaultModel = stringField(agent.defaultModel);
57
+ const models = agentModelOptions(agent.models);
30
58
  return [{
31
59
  id: agent.id,
32
60
  label: agent.label,
33
61
  ...(typeof agent.command === "string" ? { command: agent.command } : {}),
34
- ...(historyModel(agent.historyModel) ? { historyModel: historyModel(agent.historyModel) } : {}),
62
+ ...(agentHistoryModel ? { historyModel: agentHistoryModel } : {}),
63
+ ...(defaultModel ? { defaultModel } : {}),
64
+ ...(models ? { models } : {}),
35
65
  ...(isRecord(agent.sessionPrimitives)
36
66
  ? {
37
67
  sessionPrimitives: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.254",
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
  },
@@ -360,8 +380,8 @@ function modelOptionsFromEnv(value) {
360
380
  });
361
381
  }
362
382
  function modelOptionsFromCodexCache(codexHome) {
363
- const cachePath = path.join(codexHome?.trim() || path.join(os.homedir(), ".codex"), "models_cache.json");
364
- if (!existsSync(cachePath))
383
+ const cachePath = codexModelCachePaths(codexHome).find((candidate) => existsSync(candidate));
384
+ if (!cachePath)
365
385
  return [];
366
386
  try {
367
387
  const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
@@ -388,6 +408,26 @@ function modelOptionsFromCodexCache(codexHome) {
388
408
  return [];
389
409
  }
390
410
  }
411
+ function codexModelCachePaths(codexHome) {
412
+ const candidates = [
413
+ process.env.HAPPY_ELVES_CODEX_MODELS_CACHE,
414
+ codexHome?.trim() ? path.join(codexHome.trim(), "models_cache.json") : undefined,
415
+ process.env.APPDATA ? path.join(process.env.APPDATA, "Codex", "models_cache.json") : undefined,
416
+ process.env.APPDATA ? path.join(process.env.APPDATA, "codex", "models_cache.json") : undefined,
417
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "Codex", "models_cache.json") : undefined,
418
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "codex", "models_cache.json") : undefined,
419
+ path.join(os.homedir(), ".codex", "models_cache.json"),
420
+ process.env.USERPROFILE ? path.join(process.env.USERPROFILE, ".codex", "models_cache.json") : undefined,
421
+ ];
422
+ const seen = new Set();
423
+ return candidates.flatMap((candidate) => {
424
+ const normalized = candidate?.trim();
425
+ if (!normalized || seen.has(normalized))
426
+ return [];
427
+ seen.add(normalized);
428
+ return [normalized];
429
+ });
430
+ }
391
431
  function isRecord(value) {
392
432
  return typeof value === "object" && value !== null && !Array.isArray(value);
393
433
  }