toc-ocra 0.2.0

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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/assets/icon.svg +1 -0
  4. package/dist/plugin/renderer.css +2 -0
  5. package/dist/plugin/renderer.css.map +7 -0
  6. package/dist/plugin/renderer.js +2 -0
  7. package/dist/plugin/renderer.js.map +7 -0
  8. package/dist/src/config.d.ts +66 -0
  9. package/dist/src/config.js +173 -0
  10. package/dist/src/config.js.map +1 -0
  11. package/dist/src/control/pairing.d.ts +44 -0
  12. package/dist/src/control/pairing.js +99 -0
  13. package/dist/src/control/pairing.js.map +1 -0
  14. package/dist/src/exec/script-runner.d.ts +9 -0
  15. package/dist/src/exec/script-runner.js +110 -0
  16. package/dist/src/exec/script-runner.js.map +1 -0
  17. package/dist/src/health/server.d.ts +74 -0
  18. package/dist/src/health/server.js +248 -0
  19. package/dist/src/health/server.js.map +1 -0
  20. package/dist/src/index.d.ts +2 -0
  21. package/dist/src/index.js +173 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/logging/diagnostic-log.d.ts +16 -0
  24. package/dist/src/logging/diagnostic-log.js +128 -0
  25. package/dist/src/logging/diagnostic-log.js.map +1 -0
  26. package/dist/src/providers/cli/claude.d.ts +2 -0
  27. package/dist/src/providers/cli/claude.js +48 -0
  28. package/dist/src/providers/cli/claude.js.map +1 -0
  29. package/dist/src/providers/cli/codex.d.ts +2 -0
  30. package/dist/src/providers/cli/codex.js +67 -0
  31. package/dist/src/providers/cli/codex.js.map +1 -0
  32. package/dist/src/providers/cli/gemini.d.ts +2 -0
  33. package/dist/src/providers/cli/gemini.js +30 -0
  34. package/dist/src/providers/cli/gemini.js.map +1 -0
  35. package/dist/src/providers/cli/grok.d.ts +2 -0
  36. package/dist/src/providers/cli/grok.js +29 -0
  37. package/dist/src/providers/cli/grok.js.map +1 -0
  38. package/dist/src/providers/cli/opencode.d.ts +2 -0
  39. package/dist/src/providers/cli/opencode.js +29 -0
  40. package/dist/src/providers/cli/opencode.js.map +1 -0
  41. package/dist/src/providers/detection.d.ts +11 -0
  42. package/dist/src/providers/detection.js +96 -0
  43. package/dist/src/providers/detection.js.map +1 -0
  44. package/dist/src/providers/model-catalog-collector.d.ts +22 -0
  45. package/dist/src/providers/model-catalog-collector.js +66 -0
  46. package/dist/src/providers/model-catalog-collector.js.map +1 -0
  47. package/dist/src/providers/model-catalog.d.ts +45 -0
  48. package/dist/src/providers/model-catalog.js +382 -0
  49. package/dist/src/providers/model-catalog.js.map +1 -0
  50. package/dist/src/providers/provider.d.ts +49 -0
  51. package/dist/src/providers/provider.js +63 -0
  52. package/dist/src/providers/provider.js.map +1 -0
  53. package/dist/src/providers/registry.d.ts +31 -0
  54. package/dist/src/providers/registry.js +60 -0
  55. package/dist/src/providers/registry.js.map +1 -0
  56. package/dist/src/runs/orphan-registry.d.ts +24 -0
  57. package/dist/src/runs/orphan-registry.js +63 -0
  58. package/dist/src/runs/orphan-registry.js.map +1 -0
  59. package/dist/src/runs/process-tree.d.ts +21 -0
  60. package/dist/src/runs/process-tree.js +95 -0
  61. package/dist/src/runs/process-tree.js.map +1 -0
  62. package/dist/src/runs/run-supervisor.d.ts +63 -0
  63. package/dist/src/runs/run-supervisor.js +285 -0
  64. package/dist/src/runs/run-supervisor.js.map +1 -0
  65. package/dist/src/runtime/agent-runtime.d.ts +68 -0
  66. package/dist/src/runtime/agent-runtime.js +340 -0
  67. package/dist/src/runtime/agent-runtime.js.map +1 -0
  68. package/dist/src/runtime/agent-service.d.ts +33 -0
  69. package/dist/src/runtime/agent-service.js +181 -0
  70. package/dist/src/runtime/agent-service.js.map +1 -0
  71. package/dist/src/runtime/cli/hydrate-shell-path.d.ts +33 -0
  72. package/dist/src/runtime/cli/hydrate-shell-path.js +173 -0
  73. package/dist/src/runtime/cli/hydrate-shell-path.js.map +1 -0
  74. package/dist/src/session/terminal-line-sanitizer.d.ts +7 -0
  75. package/dist/src/session/terminal-line-sanitizer.js +21 -0
  76. package/dist/src/session/terminal-line-sanitizer.js.map +1 -0
  77. package/dist/src/session/terminal-tail-buffer.d.ts +70 -0
  78. package/dist/src/session/terminal-tail-buffer.js +149 -0
  79. package/dist/src/session/terminal-tail-buffer.js.map +1 -0
  80. package/dist/src/shared/cli-detection-types.d.ts +19 -0
  81. package/dist/src/shared/cli-detection-types.js +2 -0
  82. package/dist/src/shared/cli-detection-types.js.map +1 -0
  83. package/dist/src/shared/node-cli-command-resolution.d.ts +19 -0
  84. package/dist/src/shared/node-cli-command-resolution.js +173 -0
  85. package/dist/src/shared/node-cli-command-resolution.js.map +1 -0
  86. package/dist/src/transport/gateway-client.d.ts +38 -0
  87. package/dist/src/transport/gateway-client.js +73 -0
  88. package/dist/src/transport/gateway-client.js.map +1 -0
  89. package/dist/src/types/protocol.d.ts +172 -0
  90. package/dist/src/types/protocol.js +55 -0
  91. package/dist/src/types/protocol.js.map +1 -0
  92. package/dist/src/workspace/directory-browser.d.ts +17 -0
  93. package/dist/src/workspace/directory-browser.js +47 -0
  94. package/dist/src/workspace/directory-browser.js.map +1 -0
  95. package/dist/src/workspace/workspace-manager.d.ts +41 -0
  96. package/dist/src/workspace/workspace-manager.js +436 -0
  97. package/dist/src/workspace/workspace-manager.js.map +1 -0
  98. package/locales/en.json +66 -0
  99. package/locales/zh-CN.json +66 -0
  100. package/package.json +90 -0
  101. package/scripts/service.mjs +174 -0
@@ -0,0 +1,96 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { hydrateShellPath, mergePathSegments } from '../runtime/cli/hydrate-shell-path.js';
3
+ import { CLI_CATALOG, DETECTION_COMMANDS, getProviderDetectionProbeCommands, resolveDetectedProviderCommands } from './registry.js';
4
+ import { resolveCliCommandDetailed } from '../shared/node-cli-command-resolution.js';
5
+ function probeVersion(path, args) {
6
+ if (args.length === 0)
7
+ return Promise.resolve({});
8
+ return new Promise((resolve) => {
9
+ const child = spawn(path, [...args], { stdio: ['ignore', 'pipe', 'pipe'], shell: false });
10
+ let stdout = '';
11
+ let stderr = '';
12
+ let settled = false;
13
+ let timer;
14
+ const finish = (result) => {
15
+ if (settled)
16
+ return;
17
+ settled = true;
18
+ if (timer)
19
+ clearTimeout(timer);
20
+ resolve(result);
21
+ };
22
+ timer = setTimeout(() => {
23
+ child.kill('SIGKILL');
24
+ finish({ detail: 'version probe timeout' });
25
+ }, 5_000);
26
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
27
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
28
+ child.on('error', (error) => finish({ detail: error.message }));
29
+ child.on('close', (code) => {
30
+ const detail = (stdout || stderr || `exit ${code ?? 'unknown'}`).trim().slice(0, 200);
31
+ const version = stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
32
+ finish(version ? { version, detail } : { detail });
33
+ });
34
+ });
35
+ }
36
+ export class CliDetectionService {
37
+ snapshot;
38
+ pending;
39
+ async detect(forceRefresh = false) {
40
+ if (this.pending)
41
+ return this.pending;
42
+ if (!forceRefresh && this.snapshot)
43
+ return this.snapshot;
44
+ this.pending = this.detectNow(forceRefresh).finally(() => {
45
+ this.pending = undefined;
46
+ });
47
+ return this.pending;
48
+ }
49
+ async detectNow(forceRefresh) {
50
+ const hydration = await hydrateShellPath({ force: forceRefresh });
51
+ const addedPathSegments = hydration.ok ? mergePathSegments(hydration.segments) : [];
52
+ const commands = getProviderDetectionProbeCommands(DETECTION_COMMANDS);
53
+ const resolved = new Map(commands.map((command) => [command, resolveCliCommandDetailed(command)]));
54
+ const foundCommands = new Set(commands.filter((command) => resolved.get(command)?.found));
55
+ const detected = resolveDetectedProviderCommands(foundCommands, DETECTION_COMMANDS);
56
+ const providers = await Promise.all(CLI_CATALOG.map(async (config) => {
57
+ const matchedDetectionCommand = detected.get(config.id);
58
+ const result = matchedDetectionCommand ? resolved.get(matchedDetectionCommand) : undefined;
59
+ const executablePath = result?.found ? result.path : undefined;
60
+ const installed = Boolean(executablePath);
61
+ const unsupported = config.detectUnsupportedRuntimes?.includes(process.platform) ?? false;
62
+ const diagnostic = executablePath ? await probeVersion(executablePath, config.versionArguments ?? []) : {};
63
+ const detail = installed
64
+ ? diagnostic.detail
65
+ : unsupported
66
+ ? 'unsupported runtime'
67
+ : config.detectRequiredCommands?.length
68
+ ? 'command or required command not found'
69
+ : 'command not found';
70
+ return {
71
+ id: config.id,
72
+ installed,
73
+ available: installed && Boolean(config.headless),
74
+ ...(matchedDetectionCommand ? { matchedDetectionCommand } : {}),
75
+ ...(result?.path ? { resolvedPath: result.path } : {}),
76
+ ...(diagnostic.version ? { version: diagnostic.version } : {}),
77
+ ...(detail ? { detail } : {})
78
+ };
79
+ }));
80
+ this.snapshot = {
81
+ providers,
82
+ detectedIds: providers.filter((provider) => provider.installed).map((provider) => provider.id),
83
+ addedPathSegments,
84
+ shellHydrationOk: hydration.ok,
85
+ pathSource: hydration.ok ? 'shell_hydrated' : 'process_path',
86
+ pathFailureReason: hydration.failureReason,
87
+ refreshedAt: new Date().toISOString()
88
+ };
89
+ return this.snapshot;
90
+ }
91
+ }
92
+ export const cliDetection = new CliDetectionService();
93
+ export function providerCapabilityIds(snapshot) {
94
+ return snapshot.detectedIds;
95
+ }
96
+ //# sourceMappingURL=detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../../src/providers/detection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { ModelCatalogReport } from '../types/protocol.js';
2
+ import { type ModelSource } from './model-catalog.js';
3
+ /**
4
+ * Collects the model catalogs of the CLIs installed on this device.
5
+ *
6
+ * A catalog is re-read only when one of its source's watch files changed or
7
+ * its refresh interval elapsed, so calling `collect` every heartbeat is cheap.
8
+ * The report carries a content hash: the runtime sends it to the server only
9
+ * when the hash differs from the last one the server accepted.
10
+ */
11
+ export declare class ModelCatalogCollector {
12
+ private readonly sources;
13
+ private readonly cache;
14
+ constructor(sources?: Readonly<Record<string, ModelSource>>);
15
+ /** CLI ids that have a model source, among the detected ones. */
16
+ supported(detectedIds: readonly string[]): string[];
17
+ collect(detectedIds: readonly string[], options?: {
18
+ force?: boolean;
19
+ only?: string;
20
+ }): Promise<ModelCatalogReport>;
21
+ private readOne;
22
+ }
@@ -0,0 +1,66 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { stat } from 'node:fs/promises';
3
+ import { MODEL_SOURCES } from './model-catalog.js';
4
+ async function fileStamp(path) {
5
+ try {
6
+ const info = await stat(path);
7
+ return `${path}:${info.mtimeMs}:${info.size}`;
8
+ }
9
+ catch {
10
+ return `${path}:-`;
11
+ }
12
+ }
13
+ /** Stable content hash: ignores fetched_at so an unchanged list keeps its hash. */
14
+ function reportHash(items) {
15
+ const stable = items.map((item) => ({
16
+ agent_type: item.agent_type,
17
+ error: item.error ?? '',
18
+ ...(item.catalog ? { catalog: { ...item.catalog, fetched_at: '' } } : {})
19
+ }));
20
+ return createHash('sha256').update(JSON.stringify(stable)).digest('hex');
21
+ }
22
+ /**
23
+ * Collects the model catalogs of the CLIs installed on this device.
24
+ *
25
+ * A catalog is re-read only when one of its source's watch files changed or
26
+ * its refresh interval elapsed, so calling `collect` every heartbeat is cheap.
27
+ * The report carries a content hash: the runtime sends it to the server only
28
+ * when the hash differs from the last one the server accepted.
29
+ */
30
+ export class ModelCatalogCollector {
31
+ sources;
32
+ cache = new Map();
33
+ constructor(sources = MODEL_SOURCES) {
34
+ this.sources = sources;
35
+ }
36
+ /** CLI ids that have a model source, among the detected ones. */
37
+ supported(detectedIds) {
38
+ return [...new Set(detectedIds.map((id) => (id === 'claude-code' ? 'claude' : id)))].filter((id) => id in this.sources).sort();
39
+ }
40
+ async collect(detectedIds, options = {}) {
41
+ const ids = this.supported(detectedIds);
42
+ const items = await Promise.all(ids.map((id) => this.readOne(id, Boolean(options.force && (!options.only || options.only === id)))));
43
+ return { hash: reportHash(items), items };
44
+ }
45
+ async readOne(id, force) {
46
+ const source = this.sources[id];
47
+ const now = Date.now();
48
+ const stamps = await Promise.all(source.watchFiles().map(fileStamp));
49
+ const fingerprint = stamps.join('|');
50
+ const cached = this.cache.get(id);
51
+ const expired = source.refreshEveryMs !== undefined && cached !== undefined && now - cached.readAt >= source.refreshEveryMs;
52
+ if (cached && !force && !expired && cached.fingerprint === fingerprint)
53
+ return cached.item;
54
+ let item;
55
+ try {
56
+ const catalog = await source.read();
57
+ item = { agent_type: id, catalog: { ...catalog, agent_type: id } };
58
+ }
59
+ catch (error) {
60
+ item = { agent_type: id, error: error instanceof Error ? error.message : String(error) };
61
+ }
62
+ this.cache.set(id, { fingerprint, readAt: now, item });
63
+ return item;
64
+ }
65
+ }
66
+ //# sourceMappingURL=model-catalog-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-catalog-collector.js","sourceRoot":"","sources":["../../../src/providers/model-catalog-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { AgentCliModelCatalog } from '../types/protocol.js';
2
+ /**
3
+ * Claude Code builds its interactive model picker from the local
4
+ * ANTHROPIC_DEFAULT_<alias>_MODEL mappings. Read those same mappings rather
5
+ * than maintaining a provider-side list that inevitably drifts from the
6
+ * installed CLI configuration.
7
+ */
8
+ export declare function parseClaudeModelCatalog(settings: unknown, environment?: NodeJS.ProcessEnv, fetchedAt?: string): AgentCliModelCatalog;
9
+ export declare function readClaudeModelCatalog(): Promise<AgentCliModelCatalog>;
10
+ /**
11
+ * Codex writes the locally available model metadata to models_cache.json. This is
12
+ * intentionally read on the device, so the server never guesses another machine's
13
+ * model access or supported reasoning levels.
14
+ */
15
+ export declare function readCodexModelCatalog(): Promise<AgentCliModelCatalog>;
16
+ /** Normalizes the JSON emitted by `kiro-cli chat --list-models --format json`. */
17
+ export declare function parseKiroModelCatalog(payload: unknown, fetchedAt?: string): AgentCliModelCatalog;
18
+ /** Runs a CLI's model-list command and returns stdout; fails on timeout or a non-zero exit. */
19
+ export declare function runModelListCommand(executable: string, args: readonly string[], label: string): Promise<string>;
20
+ /**
21
+ * Kiro is queried from the physical device. Its current CLI returns models but
22
+ * does not advertise reasoning efforts, so the empty effort lists deliberately
23
+ * preserve Kiro's device default rather than inventing unsupported choices.
24
+ */
25
+ export declare function readKiroModelCatalog(): Promise<AgentCliModelCatalog>;
26
+ export declare function parseGeminiModelCatalog(payload: unknown, defaultModel?: string, fetchedAt?: string): AgentCliModelCatalog;
27
+ export declare function readGeminiModelCatalog(): Promise<AgentCliModelCatalog>;
28
+ export declare function parseGrokModelCatalog(output: string, config?: string, fetchedAt?: string): AgentCliModelCatalog;
29
+ export declare function readGrokModelCatalog(): Promise<AgentCliModelCatalog>;
30
+ /** `opencode models` prints one `provider/model` id per line. */
31
+ export declare function parseOpencodeModelCatalog(output: string, config?: string, fetchedAt?: string): AgentCliModelCatalog;
32
+ export declare function readOpencodeModelCatalog(): Promise<AgentCliModelCatalog>;
33
+ /**
34
+ * Where a CLI's model list comes from. `watchFiles` are the local files whose
35
+ * change means the list may have changed (checked by mtime every heartbeat);
36
+ * sources that also ask the CLI or a remote API are re-read every
37
+ * `refreshEveryMs` even when no file changed.
38
+ */
39
+ export type ModelSource = {
40
+ read: () => Promise<AgentCliModelCatalog>;
41
+ watchFiles: () => string[];
42
+ refreshEveryMs?: number;
43
+ };
44
+ export declare const MODEL_SOURCES: Readonly<Record<string, ModelSource>>;
45
+ export declare function readAgentModelCatalog(agentType: string): Promise<AgentCliModelCatalog>;
@@ -0,0 +1,382 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ import { resolveCliCommand } from '../shared/node-cli-command-resolution.js';
6
+ function stringValue(value) {
7
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
8
+ }
9
+ const CLAUDE_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
10
+ /**
11
+ * Claude Code builds its interactive model picker from the local
12
+ * ANTHROPIC_DEFAULT_<alias>_MODEL mappings. Read those same mappings rather
13
+ * than maintaining a provider-side list that inevitably drifts from the
14
+ * installed CLI configuration.
15
+ */
16
+ export function parseClaudeModelCatalog(settings, environment = process.env, fetchedAt = new Date().toISOString()) {
17
+ const source = settings && typeof settings === 'object' ? settings : {};
18
+ const configuredEnv = source.env && typeof source.env === 'object'
19
+ ? source.env
20
+ : {};
21
+ const mergedEnv = { ...configuredEnv, ...environment };
22
+ const models = new Map();
23
+ for (const [key, rawModel] of Object.entries(mergedEnv)) {
24
+ const match = key.match(/^ANTHROPIC_DEFAULT_([A-Z0-9_]+)_MODEL$/);
25
+ const id = stringValue(rawModel);
26
+ if (!match || !id)
27
+ continue;
28
+ const configuredName = stringValue(mergedEnv[`${key}_NAME`]);
29
+ const existing = models.get(id);
30
+ if (!existing) {
31
+ models.set(id, {
32
+ id,
33
+ name: configuredName ?? id,
34
+ supported_reasoning_efforts: [...CLAUDE_REASONING_EFFORTS]
35
+ });
36
+ }
37
+ }
38
+ const catalog = {
39
+ agent_type: 'claude',
40
+ fetched_at: fetchedAt,
41
+ models: [...models.values()]
42
+ };
43
+ const defaultModel = stringValue(mergedEnv.ANTHROPIC_MODEL);
44
+ if (defaultModel)
45
+ catalog.default_model = defaultModel;
46
+ return catalog;
47
+ }
48
+ const claudeHome = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude');
49
+ const codexHome = () => process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
50
+ const geminiHome = () => process.env.GEMINI_CLI_HOME?.trim() || join(homedir(), '.gemini');
51
+ const grokHome = () => process.env.GROK_HOME?.trim() || join(homedir(), '.grok');
52
+ const opencodeHome = () => join(process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config'), 'opencode');
53
+ export async function readClaudeModelCatalog() {
54
+ const home = claudeHome();
55
+ const content = await readFile(join(home, 'settings.json'), 'utf8').catch(() => '');
56
+ let settings = {};
57
+ if (content) {
58
+ try {
59
+ settings = JSON.parse(content);
60
+ }
61
+ catch {
62
+ throw new Error('Claude 本机 settings.json 不是有效 JSON');
63
+ }
64
+ }
65
+ const catalog = parseClaudeModelCatalog(settings);
66
+ if (catalog.models.length === 0) {
67
+ throw new Error('Claude 本机未配置可发现的模型映射');
68
+ }
69
+ return catalog;
70
+ }
71
+ function configValue(content, key) {
72
+ const match = content.match(new RegExp(`^\\s*${key}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`, 'm'));
73
+ return match?.[1]?.trim() || undefined;
74
+ }
75
+ function toModel(entry) {
76
+ const id = stringValue(entry.slug);
77
+ if (!id)
78
+ return undefined;
79
+ const supported = Array.isArray(entry.supported_reasoning_levels)
80
+ ? entry.supported_reasoning_levels.flatMap((value) => {
81
+ const level = stringValue(value) ?? (value && typeof value === 'object'
82
+ ? stringValue(value.effort)
83
+ : undefined);
84
+ return level ? [level] : [];
85
+ })
86
+ : [];
87
+ const defaultReasoning = stringValue(entry.default_reasoning_level);
88
+ const description = stringValue(entry.description);
89
+ const model = {
90
+ id,
91
+ name: stringValue(entry.display_name) ?? id,
92
+ supported_reasoning_efforts: supported
93
+ };
94
+ if (description)
95
+ model.description = description;
96
+ if (defaultReasoning)
97
+ model.default_reasoning_effort = defaultReasoning;
98
+ return model;
99
+ }
100
+ function parseCodexModels(content) {
101
+ if (!content)
102
+ return undefined;
103
+ try {
104
+ const parsed = JSON.parse(content);
105
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.models)) {
106
+ return undefined;
107
+ }
108
+ return parsed.models.flatMap((entry) => {
109
+ const model = toModel(entry);
110
+ return model ? [model] : [];
111
+ });
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ async function readCodexModels(codexHome, config) {
118
+ const configuredCatalog = configValue(config, 'model_catalog_json');
119
+ if (configuredCatalog) {
120
+ const catalogPath = isAbsolute(configuredCatalog)
121
+ ? configuredCatalog
122
+ : resolve(codexHome, configuredCatalog);
123
+ const configuredModels = parseCodexModels(await readFile(catalogPath, 'utf8').catch(() => ''));
124
+ if (configuredModels?.length)
125
+ return configuredModels;
126
+ }
127
+ return parseCodexModels(await readFile(join(codexHome, 'models_cache.json'), 'utf8').catch(() => '')) ?? [];
128
+ }
129
+ /**
130
+ * Codex writes the locally available model metadata to models_cache.json. This is
131
+ * intentionally read on the device, so the server never guesses another machine's
132
+ * model access or supported reasoning levels.
133
+ */
134
+ export async function readCodexModelCatalog() {
135
+ const home = codexHome();
136
+ const config = await readFile(join(home, 'config.toml'), 'utf8').catch(() => '');
137
+ const models = await readCodexModels(home, config);
138
+ const defaultModel = configValue(config, 'model');
139
+ const defaultReasoningEffort = configValue(config, 'model_reasoning_effort');
140
+ const catalog = {
141
+ agent_type: 'codex',
142
+ fetched_at: new Date().toISOString(),
143
+ models
144
+ };
145
+ if (defaultModel)
146
+ catalog.default_model = defaultModel;
147
+ if (defaultReasoningEffort)
148
+ catalog.default_reasoning_effort = defaultReasoningEffort;
149
+ return catalog;
150
+ }
151
+ /** Normalizes the JSON emitted by `kiro-cli chat --list-models --format json`. */
152
+ export function parseKiroModelCatalog(payload, fetchedAt = new Date().toISOString()) {
153
+ const source = payload && typeof payload === 'object' ? payload : {};
154
+ const models = Array.isArray(source.models)
155
+ ? source.models.flatMap((item) => {
156
+ const entry = item && typeof item === 'object' ? item : {};
157
+ const id = stringValue(entry.model_id) ?? stringValue(entry.model_name);
158
+ if (!id)
159
+ return [];
160
+ const model = {
161
+ id,
162
+ name: stringValue(entry.model_name) ?? id,
163
+ supported_reasoning_efforts: []
164
+ };
165
+ const description = stringValue(entry.description);
166
+ if (description)
167
+ model.description = description;
168
+ return [model];
169
+ })
170
+ : [];
171
+ const catalog = {
172
+ agent_type: 'kiro',
173
+ fetched_at: fetchedAt,
174
+ models
175
+ };
176
+ const defaultModel = stringValue(source.default_model);
177
+ if (defaultModel)
178
+ catalog.default_model = defaultModel;
179
+ return catalog;
180
+ }
181
+ const MODEL_LIST_TIMEOUT_MS = 20_000;
182
+ /** Runs a CLI's model-list command and returns stdout; fails on timeout or a non-zero exit. */
183
+ export function runModelListCommand(executable, args, label) {
184
+ return new Promise((resolve, reject) => {
185
+ const child = spawn(executable, [...args], { shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
186
+ let stdout = '';
187
+ let stderr = '';
188
+ const timer = setTimeout(() => {
189
+ child.kill('SIGKILL');
190
+ reject(new Error(`${label} 读取模型列表超时`));
191
+ }, MODEL_LIST_TIMEOUT_MS);
192
+ child.stdout.setEncoding('utf8');
193
+ child.stderr.setEncoding('utf8');
194
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
195
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
196
+ child.once('error', (error) => { clearTimeout(timer); reject(error); });
197
+ child.once('close', (code) => {
198
+ clearTimeout(timer);
199
+ if (code === 0)
200
+ resolve(stdout);
201
+ else
202
+ reject(new Error((stderr || stdout || `${label} exited with code ${code ?? 'unknown'}`).trim()));
203
+ });
204
+ });
205
+ }
206
+ /**
207
+ * Kiro is queried from the physical device. Its current CLI returns models but
208
+ * does not advertise reasoning efforts, so the empty effort lists deliberately
209
+ * preserve Kiro's device default rather than inventing unsupported choices.
210
+ */
211
+ export async function readKiroModelCatalog() {
212
+ const executable = resolveCliCommand('kiro-cli');
213
+ const output = await runModelListCommand(executable, ['chat', '--list-models', '--format', 'json'], 'kiro-cli');
214
+ let payload;
215
+ try {
216
+ payload = JSON.parse(output);
217
+ }
218
+ catch {
219
+ throw new Error('kiro-cli 返回的模型目录不是有效 JSON');
220
+ }
221
+ return parseKiroModelCatalog(payload);
222
+ }
223
+ function parseEnvFile(content) {
224
+ const values = {};
225
+ for (const line of content.split(/\r?\n/)) {
226
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
227
+ const key = match?.[1];
228
+ let value = match?.[2];
229
+ if (!key || value === undefined)
230
+ continue;
231
+ if ((value.startsWith('"') && value.endsWith('"')) ||
232
+ (value.startsWith("'") && value.endsWith("'"))) {
233
+ value = value.slice(1, -1);
234
+ }
235
+ values[key] = value;
236
+ }
237
+ return values;
238
+ }
239
+ export function parseGeminiModelCatalog(payload, defaultModel, fetchedAt = new Date().toISOString()) {
240
+ const source = payload && typeof payload === 'object'
241
+ ? payload
242
+ : {};
243
+ const models = Array.isArray(source.models)
244
+ ? source.models.flatMap((item) => {
245
+ const entry = item && typeof item === 'object' ? item : {};
246
+ const rawId = stringValue(entry.name);
247
+ if (!rawId)
248
+ return [];
249
+ const id = rawId.replace(/^models\//, '');
250
+ const model = {
251
+ id,
252
+ name: stringValue(entry.displayName) ?? id,
253
+ supported_reasoning_efforts: []
254
+ };
255
+ const description = stringValue(entry.description);
256
+ if (description)
257
+ model.description = description;
258
+ return [model];
259
+ })
260
+ : [];
261
+ const catalog = {
262
+ agent_type: 'gemini',
263
+ fetched_at: fetchedAt,
264
+ models
265
+ };
266
+ if (defaultModel?.trim())
267
+ catalog.default_model = defaultModel.trim();
268
+ return catalog;
269
+ }
270
+ function geminiModelsUrl(baseUrl) {
271
+ const url = new URL(baseUrl);
272
+ const path = url.pathname.replace(/\/+$/, '');
273
+ url.pathname = path.endsWith('/v1beta') ? `${path}/models` : `${path}/v1beta/models`;
274
+ url.search = '';
275
+ return url;
276
+ }
277
+ export async function readGeminiModelCatalog() {
278
+ const fileEnvironment = parseEnvFile(await readFile(join(geminiHome(), '.env'), 'utf8').catch(() => ''));
279
+ const environment = { ...fileEnvironment, ...process.env };
280
+ const baseUrl = environment.GOOGLE_GEMINI_BASE_URL?.trim();
281
+ const apiKey = environment.GEMINI_API_KEY?.trim();
282
+ if (!baseUrl)
283
+ throw new Error('Gemini 本机未配置 GOOGLE_GEMINI_BASE_URL');
284
+ if (!apiKey)
285
+ throw new Error('Gemini 本机未配置 GEMINI_API_KEY');
286
+ const url = geminiModelsUrl(baseUrl);
287
+ url.searchParams.set('key', apiKey);
288
+ const response = await fetch(url, {
289
+ headers: { 'x-goog-api-key': apiKey }
290
+ });
291
+ if (!response.ok) {
292
+ throw new Error(`Gemini 模型目录请求失败 (${response.status})`);
293
+ }
294
+ return parseGeminiModelCatalog(await response.json(), environment.GEMINI_MODEL);
295
+ }
296
+ const GROK_REASONING_EFFORTS = ['low', 'medium', 'high'];
297
+ export function parseGrokModelCatalog(output, config = '', fetchedAt = new Date().toISOString()) {
298
+ const plain = output.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
299
+ const defaultModel = plain.match(/^\s*Default model:\s*(\S+)\s*$/m)?.[1]
300
+ ?? configValue(config, 'default');
301
+ const availableBlock = plain.split(/^\s*Available models:\s*$/m)[1] ?? '';
302
+ const ids = [...new Set(availableBlock.split(/\r?\n/).flatMap((line) => {
303
+ const match = line.match(/^\s*(?:\*|-)\s+(\S+)(?:\s+\(default\))?\s*$/);
304
+ const id = match?.[1];
305
+ return id ? [id] : [];
306
+ }))];
307
+ const catalog = {
308
+ agent_type: 'grok',
309
+ fetched_at: fetchedAt,
310
+ models: ids.map((id) => ({
311
+ id,
312
+ name: id,
313
+ supported_reasoning_efforts: [...GROK_REASONING_EFFORTS]
314
+ }))
315
+ };
316
+ if (defaultModel)
317
+ catalog.default_model = defaultModel;
318
+ const defaultReasoningEffort = configValue(config, 'default_reasoning_effort');
319
+ if (defaultReasoningEffort)
320
+ catalog.default_reasoning_effort = defaultReasoningEffort;
321
+ return catalog;
322
+ }
323
+ export async function readGrokModelCatalog() {
324
+ const config = await readFile(join(grokHome(), 'config.toml'), 'utf8').catch(() => '');
325
+ const output = await runModelListCommand(resolveCliCommand('grok'), ['models'], 'grok');
326
+ const catalog = parseGrokModelCatalog(output, config);
327
+ if (catalog.models.length === 0)
328
+ throw new Error('Grok 本机未返回可用模型');
329
+ return catalog;
330
+ }
331
+ /** `opencode models` prints one `provider/model` id per line. */
332
+ export function parseOpencodeModelCatalog(output, config = '', fetchedAt = new Date().toISOString()) {
333
+ const plain = output.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
334
+ const ids = [...new Set(plain.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^[^\s/]+\/\S+$/.test(line)))];
335
+ const catalog = {
336
+ agent_type: 'opencode',
337
+ fetched_at: fetchedAt,
338
+ models: ids.map((id) => ({ id, name: id, supported_reasoning_efforts: [] }))
339
+ };
340
+ const defaultModel = config.match(/"model"\s*:\s*"([^"]+)"/)?.[1]?.trim();
341
+ if (defaultModel)
342
+ catalog.default_model = defaultModel;
343
+ return catalog;
344
+ }
345
+ export async function readOpencodeModelCatalog() {
346
+ const config = await readFile(join(opencodeHome(), 'opencode.json'), 'utf8').catch(() => '')
347
+ || await readFile(join(opencodeHome(), 'opencode.jsonc'), 'utf8').catch(() => '');
348
+ const output = await runModelListCommand(resolveCliCommand('opencode'), ['models'], 'opencode');
349
+ const catalog = parseOpencodeModelCatalog(output, config);
350
+ if (catalog.models.length === 0)
351
+ throw new Error('OpenCode 本机未返回可用模型');
352
+ return catalog;
353
+ }
354
+ const TEN_MINUTES = 10 * 60 * 1_000;
355
+ export const MODEL_SOURCES = {
356
+ claude: { read: readClaudeModelCatalog, watchFiles: () => [join(claudeHome(), 'settings.json')] },
357
+ codex: {
358
+ read: readCodexModelCatalog,
359
+ watchFiles: () => {
360
+ const home = codexHome();
361
+ return [join(home, 'config.toml'), join(home, 'models_cache.json')];
362
+ },
363
+ // model_catalog_json may point anywhere; a periodic re-read covers it.
364
+ refreshEveryMs: TEN_MINUTES
365
+ },
366
+ gemini: { read: readGeminiModelCatalog, watchFiles: () => [join(geminiHome(), '.env')], refreshEveryMs: TEN_MINUTES },
367
+ grok: { read: readGrokModelCatalog, watchFiles: () => [join(grokHome(), 'config.toml')], refreshEveryMs: TEN_MINUTES },
368
+ kiro: { read: readKiroModelCatalog, watchFiles: () => [], refreshEveryMs: TEN_MINUTES },
369
+ opencode: {
370
+ read: readOpencodeModelCatalog,
371
+ watchFiles: () => [join(opencodeHome(), 'opencode.json'), join(opencodeHome(), 'opencode.jsonc')],
372
+ refreshEveryMs: TEN_MINUTES
373
+ }
374
+ };
375
+ export async function readAgentModelCatalog(agentType) {
376
+ const id = agentType.trim().toLowerCase() === 'claude-code' ? 'claude' : agentType.trim().toLowerCase();
377
+ const source = MODEL_SOURCES[id];
378
+ if (!source)
379
+ throw new Error(`当前 Agent 暂不支持 ${agentType} 的模型目录`);
380
+ return source.read();
381
+ }
382
+ //# sourceMappingURL=model-catalog.js.map