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

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.260",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -169,6 +169,10 @@ export function createExternalAgentRuntimeProvider(options) {
169
169
  const clients = options.providers.map((config) => new ExternalProviderClient(config));
170
170
  const clientsByAgent = new Map();
171
171
  async function initialize() {
172
+ const fallback = options.fallback;
173
+ if (typeof fallback.initialize === "function") {
174
+ await fallback.initialize().catch(() => undefined);
175
+ }
172
176
  await Promise.all(clients.map(async (client) => {
173
177
  await client.initialize().catch(() => undefined);
174
178
  for (const agent of client.listAgents())
@@ -273,7 +277,11 @@ export function createExternalAgentRuntimeProvider(options) {
273
277
  return {
274
278
  initialize,
275
279
  diagnostics() {
276
- return clients.map((client) => client.diagnostics());
280
+ const fallback = options.fallback;
281
+ const fallbackDiagnostics = typeof fallback.diagnostics === "function"
282
+ ? fallback.diagnostics()
283
+ : [];
284
+ return [...fallbackDiagnostics, ...clients.map((client) => client.diagnostics())];
277
285
  },
278
286
  dispose() {
279
287
  for (const client of clients)
@@ -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.260",
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.260",
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 = 20_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,12 @@ 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
+ diagnostics(): Array<Record<string, unknown>>;
15
+ };
11
16
  //# 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,15 @@ 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();
18
+ let codexModelDiscovery = {
19
+ provider: "cli",
20
+ component: "codex-model-discovery",
21
+ status: "not-run",
22
+ source: codexOptions.models && codexOptions.models.length > 0 ? "fallback" : undefined,
23
+ modelCount: codexOptions.models?.length ?? 0,
24
+ ...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
25
+ };
16
26
  const sessions = new CliSessionStore({ cwd: options.cwd, codexCommand });
17
27
  const historySnapshotDir = path.join(options.stateDir, "history-snapshots");
18
28
  async function listHistoricalSessionPage(input) {
@@ -66,6 +76,75 @@ export function createCliRuntimeProvider(options) {
66
76
  }), loadedByBackendId);
67
77
  }
68
78
  return {
79
+ async initialize() {
80
+ const configured = agentModelsFromEnv("HAPPY_ELVES_CODEX");
81
+ if (configured.models && configured.models.length > 0) {
82
+ codexOptions = configured;
83
+ codexModelDiscovery = {
84
+ provider: "cli",
85
+ component: "codex-model-discovery",
86
+ status: "configured",
87
+ source: "env",
88
+ modelCount: configured.models.length,
89
+ ...(configured.defaultModel ? { defaultModel: configured.defaultModel } : {}),
90
+ };
91
+ return;
92
+ }
93
+ const startedAt = Date.now();
94
+ const discovered = await discoverCodexRuntimeOptions({
95
+ command: codexCommand,
96
+ cwd: options.cwd,
97
+ timeoutMs: options.codexDiscoveryTimeoutMs,
98
+ }).catch((error) => {
99
+ codexModelDiscovery = {
100
+ provider: "cli",
101
+ component: "codex-model-discovery",
102
+ status: "failed",
103
+ source: "provider",
104
+ elapsedMs: Date.now() - startedAt,
105
+ error: error instanceof Error ? error.message : String(error),
106
+ };
107
+ return undefined;
108
+ });
109
+ if (discovered?.models && discovered.models.length > 0) {
110
+ codexOptions = {
111
+ ...(configured.defaultModel ? { defaultModel: configured.defaultModel } : discovered.defaultModel ? { defaultModel: discovered.defaultModel } : {}),
112
+ models: discovered.models,
113
+ };
114
+ codexModelDiscovery = {
115
+ provider: "cli",
116
+ component: "codex-model-discovery",
117
+ status: "ok",
118
+ source: "provider",
119
+ elapsedMs: Date.now() - startedAt,
120
+ modelCount: discovered.models.length,
121
+ ...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
122
+ };
123
+ return;
124
+ }
125
+ codexOptions = codexAgentModels();
126
+ if (codexModelDiscovery.status !== "failed") {
127
+ codexModelDiscovery = {
128
+ provider: "cli",
129
+ component: "codex-model-discovery",
130
+ status: "failed",
131
+ source: "provider",
132
+ elapsedMs: Date.now() - startedAt,
133
+ error: "Codex provider model/list returned no models",
134
+ };
135
+ }
136
+ if (codexOptions.models && codexOptions.models.length > 0) {
137
+ codexModelDiscovery = {
138
+ ...codexModelDiscovery,
139
+ source: "fallback",
140
+ modelCount: codexOptions.models.length,
141
+ ...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
142
+ };
143
+ }
144
+ },
145
+ diagnostics() {
146
+ return [codexModelDiscovery];
147
+ },
69
148
  getCapabilities() {
70
149
  return {
71
150
  provider: "cli",
@@ -75,7 +154,7 @@ export function createCliRuntimeProvider(options) {
75
154
  },
76
155
  listAgents() {
77
156
  return [
78
- { id: "codex", label: "Codex", ...codexAgentModels() },
157
+ { id: "codex", label: "Codex", ...codexOptions },
79
158
  { id: "claude", label: "Claude Code", ...agentModelsFromEnv("HAPPY_ELVES_CLAUDE") },
80
159
  ];
81
160
  },