@open-agent-toolkit/cli 0.1.43 → 0.1.46
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.
- package/assets/agents/oat-reviewer.md +2 -2
- package/assets/config/dispatch-matrix-recommendation.json +23 -0
- package/assets/docs/cli-utilities/config-and-local-state.md +7 -0
- package/assets/docs/cli-utilities/configuration.md +104 -25
- package/assets/docs/cli-utilities/workflow-gates.md +79 -16
- package/assets/docs/contributing/code.md +10 -4
- package/assets/docs/contributing/hooks-and-safety.md +23 -0
- package/assets/docs/provider-sync/config.md +5 -1
- package/assets/docs/provider-sync/manifest-and-drift.md +6 -1
- package/assets/docs/provider-sync/providers.md +6 -3
- package/assets/docs/provider-sync/scope-and-surface.md +2 -1
- package/assets/docs/reference/oat-directory-structure.md +2 -2
- package/assets/docs/workflows/projects/dispatch-ceiling.md +110 -27
- package/assets/docs/workflows/projects/implementation-execution.md +20 -13
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +155 -58
- package/assets/skills/oat-project-plan/SKILL.md +22 -3
- package/assets/skills/oat-project-quick-start/SKILL.md +54 -23
- package/assets/templates/state.md +5 -0
- package/dist/commands/config/index.d.ts +6 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +429 -20
- package/dist/commands/doctor/index.d.ts +6 -1
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +209 -19
- package/dist/commands/gate/index.d.ts +39 -2
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +343 -30
- package/dist/commands/internal/cursor-current-target.d.ts +37 -0
- package/dist/commands/internal/cursor-current-target.d.ts.map +1 -0
- package/dist/commands/internal/cursor-current-target.js +228 -0
- package/dist/commands/internal/index.d.ts.map +1 -1
- package/dist/commands/internal/index.js +2 -0
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +487 -17
- package/dist/commands/providers/codex/index.d.ts +4 -0
- package/dist/commands/providers/codex/index.d.ts.map +1 -0
- package/dist/commands/providers/codex/index.js +7 -0
- package/dist/commands/providers/codex/materialize.d.ts +5 -0
- package/dist/commands/providers/codex/materialize.d.ts.map +1 -0
- package/dist/commands/providers/codex/materialize.js +152 -0
- package/dist/commands/providers/index.d.ts +2 -2
- package/dist/commands/providers/index.d.ts.map +1 -1
- package/dist/commands/providers/index.js +4 -2
- package/dist/commands/providers/providers.types.d.ts +23 -0
- package/dist/commands/providers/providers.types.d.ts.map +1 -1
- package/dist/commands/shared/codex-strays.d.ts.map +1 -1
- package/dist/commands/shared/codex-strays.js +11 -1
- package/dist/commands/status/index.d.ts +4 -1
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +1 -1
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +1 -1
- package/dist/commands/sync/sync.types.d.ts +4 -1
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/config/dispatch-ceiling-preset.d.ts +4 -0
- package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
- package/dist/config/dispatch-ceiling-preset.js +3 -0
- package/dist/config/dispatch-policy-options.d.ts +20 -0
- package/dist/config/dispatch-policy-options.d.ts.map +1 -0
- package/dist/config/dispatch-policy-options.js +96 -0
- package/dist/config/oat-config.d.ts +21 -5
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +118 -10
- package/dist/config/resolve.js +12 -0
- package/dist/manifest/manifest.types.d.ts +12 -12
- package/dist/providers/ceiling/registry.d.ts +7 -5
- package/dist/providers/ceiling/registry.d.ts.map +1 -1
- package/dist/providers/ceiling/registry.js +32 -5
- package/dist/providers/codex/codec/config-merge.d.ts +6 -0
- package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
- package/dist/providers/codex/codec/config-merge.js +7 -0
- package/dist/providers/codex/codec/materialize.d.ts +16 -0
- package/dist/providers/codex/codec/materialize.d.ts.map +1 -0
- package/dist/providers/codex/codec/materialize.js +57 -0
- package/dist/providers/codex/codec/shared.d.ts +1 -0
- package/dist/providers/codex/codec/shared.d.ts.map +1 -1
- package/dist/providers/codex/codec/shared.js +9 -1
- package/dist/providers/codex/codec/sync-extension.d.ts +7 -1
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +183 -46
- package/dist/providers/identity/availability.d.ts +45 -0
- package/dist/providers/identity/availability.d.ts.map +1 -0
- package/dist/providers/identity/availability.js +353 -0
- package/dist/providers/identity/family.d.ts +13 -0
- package/dist/providers/identity/family.d.ts.map +1 -0
- package/dist/providers/identity/family.js +32 -0
- package/dist/providers/identity/provenance.d.ts +21 -0
- package/dist/providers/identity/provenance.d.ts.map +1 -0
- package/dist/providers/identity/provenance.js +65 -0
- package/dist/providers/identity/stamp.d.ts +35 -0
- package/dist/providers/identity/stamp.d.ts.map +1 -0
- package/dist/providers/identity/stamp.js +171 -0
- package/package.json +2 -2
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
2
|
+
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseCanonicalAgentFile } from '../../../agents/canonical/index.js';
|
|
5
|
+
import { isCodexMaterializedRouteTarget, resolveActiveProject, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
|
|
6
|
+
import { resolveEffectiveConfig } from '../../../config/resolve.js';
|
|
5
7
|
import { ensureDir, fileExists } from '../../../fs/io.js';
|
|
6
8
|
import TOML from '@iarna/toml';
|
|
9
|
+
import YAML from 'yaml';
|
|
7
10
|
import { mergeCodexConfig } from './config-merge.js';
|
|
8
11
|
import { exportCanonicalAgentToCodexRole } from './export-to-codex.js';
|
|
12
|
+
import { materializeCodexRole } from './materialize.js';
|
|
9
13
|
import { isOatManagedCodexRoleFile } from './shared.js';
|
|
10
|
-
const
|
|
11
|
-
const CODEX_EFFORT_VARIANT_BASE_ROLES = new Set([
|
|
14
|
+
const CODEX_MATERIALIZED_BASE_ROLES = new Set([
|
|
12
15
|
'oat-phase-implementer',
|
|
13
16
|
'oat-reviewer',
|
|
14
17
|
]);
|
|
@@ -42,41 +45,7 @@ function normalizeManagedRolesConfig(desiredRoles) {
|
|
|
42
45
|
configFile: role.configFile,
|
|
43
46
|
}));
|
|
44
47
|
}
|
|
45
|
-
function
|
|
46
|
-
return baseContent
|
|
47
|
-
.replace(`# oat-role: ${baseRoleName}`, `# oat-role: ${variantRoleName}`)
|
|
48
|
-
.replace('developer_instructions =', `model_reasoning_effort = "${effort}"\ndeveloper_instructions =`);
|
|
49
|
-
}
|
|
50
|
-
function codexEffortVariantDescription(baseRoleName, effort) {
|
|
51
|
-
const roleLabel = baseRoleName === 'oat-reviewer' ? 'reviewer' : 'phase implementer';
|
|
52
|
-
if (effort === 'low') {
|
|
53
|
-
return `OAT ${roleLabel} pinned to low reasoning effort for narrow mechanical implementation, fix, or review phases.`;
|
|
54
|
-
}
|
|
55
|
-
if (effort === 'medium') {
|
|
56
|
-
return `OAT ${roleLabel} pinned to medium reasoning effort for normal multi-file implementation, fix, or review phases.`;
|
|
57
|
-
}
|
|
58
|
-
if (effort === 'high') {
|
|
59
|
-
return `OAT ${roleLabel} pinned to high reasoning effort for broad, subtle, or higher-risk implementation, fix, or review phases.`;
|
|
60
|
-
}
|
|
61
|
-
return `OAT ${roleLabel} pinned to xhigh reasoning effort for the highest configured Codex dispatch policy cap.`;
|
|
62
|
-
}
|
|
63
|
-
function codexEffortVariantsFromBase(exported) {
|
|
64
|
-
if (!CODEX_EFFORT_VARIANT_BASE_ROLES.has(exported.roleName)) {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
return CODEX_EFFORT_VARIANTS.map((effort) => {
|
|
68
|
-
const variantRoleName = `${exported.roleName}-${effort}`;
|
|
69
|
-
const configFile = `agents/${variantRoleName}.toml`;
|
|
70
|
-
return {
|
|
71
|
-
roleName: variantRoleName,
|
|
72
|
-
description: codexEffortVariantDescription(exported.roleName, effort),
|
|
73
|
-
configFile,
|
|
74
|
-
rolePath: join(dirname(exported.rolePath), `${variantRoleName}.toml`),
|
|
75
|
-
content: codexEffortVariantContent(exported.content, exported.roleName, variantRoleName, effort),
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
async function desiredRolesFromCanonical(canonicalEntries, scopeRoot) {
|
|
48
|
+
async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets) {
|
|
80
49
|
const roles = [];
|
|
81
50
|
for (const entry of canonicalEntries) {
|
|
82
51
|
if (entry.type !== 'agent' ||
|
|
@@ -93,10 +62,154 @@ async function desiredRolesFromCanonical(canonicalEntries, scopeRoot) {
|
|
|
93
62
|
rolePath: join(scopeRoot, '.codex', exported.configFile),
|
|
94
63
|
content: exported.content,
|
|
95
64
|
});
|
|
96
|
-
|
|
65
|
+
if (CODEX_MATERIALIZED_BASE_ROLES.has(exported.roleName)) {
|
|
66
|
+
for (const target of materializationTargets) {
|
|
67
|
+
const materialized = materializeCodexRole({
|
|
68
|
+
agent: parsed,
|
|
69
|
+
model: target.model,
|
|
70
|
+
effort: target.effort,
|
|
71
|
+
});
|
|
72
|
+
roles.push({
|
|
73
|
+
roleName: materialized.roleName,
|
|
74
|
+
description: materialized.description,
|
|
75
|
+
configFile: materialized.configFile,
|
|
76
|
+
rolePath: join(scopeRoot, '.codex', materialized.configFile),
|
|
77
|
+
content: materialized.content,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
97
81
|
}
|
|
98
82
|
return roles.sort((left, right) => left.roleName.localeCompare(right.roleName));
|
|
99
83
|
}
|
|
84
|
+
function collectCodexTargetFromEntry(entry, targets) {
|
|
85
|
+
if (typeof entry === 'string') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!isCodexMaterializedRouteTarget('codex', entry)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const validation = validateDispatchRouteTarget('codex', entry);
|
|
92
|
+
if (!validation.valid || !entry.model || !entry.effort) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
targets.set(`${entry.model}\0${entry.effort}`, {
|
|
96
|
+
model: entry.model,
|
|
97
|
+
effort: entry.effort,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function collectCodexTargetsFromCell(cell, targets) {
|
|
101
|
+
if (typeof cell === 'string') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of cell) {
|
|
105
|
+
collectCodexTargetFromEntry(entry, targets);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function collectCodexMaterializationTargetsFromProvider(providerValue, targets) {
|
|
109
|
+
if (providerValue === undefined || typeof providerValue === 'string') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const cell of Object.values(providerValue)) {
|
|
113
|
+
if (cell !== undefined) {
|
|
114
|
+
collectCodexTargetsFromCell(cell, targets);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function sortCodexMaterializationTargets(targets) {
|
|
119
|
+
return [...targets.values()].sort((left, right) => {
|
|
120
|
+
const modelOrder = left.model.localeCompare(right.model);
|
|
121
|
+
return modelOrder === 0
|
|
122
|
+
? left.effort.localeCompare(right.effort)
|
|
123
|
+
: modelOrder;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function routeTargetFromUnknown(value) {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const record = value;
|
|
131
|
+
const target = {};
|
|
132
|
+
for (const key of ['harness', 'model', 'effort']) {
|
|
133
|
+
const rawValue = record[key];
|
|
134
|
+
if (typeof rawValue === 'string' && rawValue.trim()) {
|
|
135
|
+
target[key] = rawValue.trim();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Object.keys(target).length > 0 ? target : null;
|
|
139
|
+
}
|
|
140
|
+
function collectCodexTargetsFromUnknownCell(value, targets) {
|
|
141
|
+
if (typeof value === 'string' || !Array.isArray(value)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const entry of value) {
|
|
145
|
+
if (typeof entry === 'string') {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const target = routeTargetFromUnknown(entry);
|
|
149
|
+
if (target) {
|
|
150
|
+
collectCodexTargetFromEntry(target, targets);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function collectCodexTargetsFromUnknownProvider(value, targets) {
|
|
155
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const cell of Object.values(value)) {
|
|
159
|
+
collectCodexTargetsFromUnknownCell(cell, targets);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function frontmatterBlock(content) {
|
|
163
|
+
const normalized = content.startsWith('\uFEFF') ? content.slice(1) : content;
|
|
164
|
+
if (!normalized.startsWith('---\n')) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const end = normalized.indexOf('\n---', 4);
|
|
168
|
+
return end > 0 ? normalized.slice(4, end) : null;
|
|
169
|
+
}
|
|
170
|
+
async function collectProjectStateCodexTargets(scopeRoot, options, targets) {
|
|
171
|
+
const projectPath = options.projectPath === undefined
|
|
172
|
+
? (await resolveActiveProject(scopeRoot)).path
|
|
173
|
+
: options.projectPath;
|
|
174
|
+
if (!projectPath) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const statePath = join(resolve(scopeRoot, projectPath), 'state.md');
|
|
178
|
+
const stateContent = await readOptionalFile(statePath);
|
|
179
|
+
if (!stateContent) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const frontmatter = frontmatterBlock(stateContent);
|
|
183
|
+
if (!frontmatter) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const parsed = YAML.parse(frontmatter);
|
|
187
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const policy = parsed['oat_dispatch_policy'];
|
|
191
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const matrix = policy['matrix'];
|
|
195
|
+
if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
collectCodexTargetsFromUnknownProvider(matrix['codex'], targets);
|
|
199
|
+
}
|
|
200
|
+
async function readCodexMaterializationTargets(scopeRoot, options = {}) {
|
|
201
|
+
const targets = new Map();
|
|
202
|
+
const effectiveConfig = await resolveEffectiveConfig(scopeRoot, options.userConfigDir ?? join(scopeRoot, '.oat', '__no-user-config__'), options.env);
|
|
203
|
+
for (const providerValue of [
|
|
204
|
+
effectiveConfig.user.workflow?.dispatchCeiling?.providers?.codex,
|
|
205
|
+
effectiveConfig.shared.workflow?.dispatchCeiling?.providers?.codex,
|
|
206
|
+
effectiveConfig.local.workflow?.dispatchCeiling?.providers?.codex,
|
|
207
|
+
]) {
|
|
208
|
+
collectCodexMaterializationTargetsFromProvider(providerValue, targets);
|
|
209
|
+
}
|
|
210
|
+
await collectProjectStateCodexTargets(scopeRoot, options, targets);
|
|
211
|
+
return sortCodexMaterializationTargets(targets);
|
|
212
|
+
}
|
|
100
213
|
function parseConfigAgentTable(content) {
|
|
101
214
|
if (!content || content.trim() === '') {
|
|
102
215
|
return {};
|
|
@@ -124,7 +237,7 @@ function parseConfigAgentTable(content) {
|
|
|
124
237
|
}
|
|
125
238
|
async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames) {
|
|
126
239
|
const agents = parseConfigAgentTable(existingConfigContent);
|
|
127
|
-
const stale =
|
|
240
|
+
const stale = new Set();
|
|
128
241
|
for (const [roleName, roleConfig] of Object.entries(agents)) {
|
|
129
242
|
if (desiredRoleNames.has(roleName)) {
|
|
130
243
|
continue;
|
|
@@ -138,13 +251,37 @@ async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desire
|
|
|
138
251
|
if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
|
|
139
252
|
continue;
|
|
140
253
|
}
|
|
141
|
-
stale.
|
|
254
|
+
stale.add(roleName);
|
|
255
|
+
}
|
|
256
|
+
const agentsDir = join(scopeRoot, '.codex', 'agents');
|
|
257
|
+
let roleFiles;
|
|
258
|
+
try {
|
|
259
|
+
roleFiles = await readdir(agentsDir);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
roleFiles = [];
|
|
263
|
+
}
|
|
264
|
+
for (const roleFile of roleFiles) {
|
|
265
|
+
if (!roleFile.endsWith('.toml')) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const roleName = basename(roleFile, '.toml');
|
|
269
|
+
if (desiredRoleNames.has(roleName) || stale.has(roleName)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const rolePath = join(agentsDir, roleFile);
|
|
273
|
+
const roleContent = await readOptionalFile(rolePath);
|
|
274
|
+
if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
stale.add(roleName);
|
|
142
278
|
}
|
|
143
|
-
return stale.sort((left, right) => left.localeCompare(right));
|
|
279
|
+
return [...stale].sort((left, right) => left.localeCompare(right));
|
|
144
280
|
}
|
|
145
|
-
export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths) {
|
|
281
|
+
export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths, options = {}) {
|
|
146
282
|
const isPartialSync = allowedCanonicalPaths !== undefined && allowedCanonicalPaths.length > 0;
|
|
147
|
-
const
|
|
283
|
+
const materializationTargets = await readCodexMaterializationTargets(scopeRoot, options);
|
|
284
|
+
const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot, materializationTargets);
|
|
148
285
|
const desiredRoleNames = new Set(desiredRoles.map((role) => role.roleName));
|
|
149
286
|
const existingConfigPath = configPath(scopeRoot);
|
|
150
287
|
const existingConfigContent = await readOptionalFile(existingConfigPath);
|
|
@@ -227,7 +364,7 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
|
|
|
227
364
|
});
|
|
228
365
|
return {
|
|
229
366
|
operations,
|
|
230
|
-
managedRoles: desiredRoles.map((role) => role.roleName),
|
|
367
|
+
managedRoles: [...desiredRoles.map((role) => role.roleName), ...staleRoles],
|
|
231
368
|
aggregateConfigHash: hashContent(configMerge.mergedContent),
|
|
232
369
|
};
|
|
233
370
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type MatrixCellAvailability = 'valid' | 'unknown-value' | 'unvalidated';
|
|
2
|
+
export interface MatrixCellAvailabilityResult {
|
|
3
|
+
availability: MatrixCellAvailability;
|
|
4
|
+
allowedValues?: string[];
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export type MatrixCellAvailabilityResponse = MatrixCellAvailability | MatrixCellAvailabilityResult;
|
|
8
|
+
export interface CursorAgentRunOptions {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
}
|
|
12
|
+
export interface CursorAgentRunResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CodexRunOptions {
|
|
18
|
+
cwd: string;
|
|
19
|
+
env: NodeJS.ProcessEnv;
|
|
20
|
+
}
|
|
21
|
+
export interface CodexRunResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AvailabilityOracleDependencies {
|
|
27
|
+
pathExists: (path: string) => Promise<boolean>;
|
|
28
|
+
runCursorAgent: (args: string[], options: CursorAgentRunOptions) => Promise<CursorAgentRunResult>;
|
|
29
|
+
runCodex: (args: string[], options: CodexRunOptions) => Promise<CodexRunResult>;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}
|
|
32
|
+
export interface ValidateMatrixCellOptions {
|
|
33
|
+
cwd: string;
|
|
34
|
+
detailed?: boolean;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
dependencies?: Partial<AvailabilityOracleDependencies>;
|
|
37
|
+
target?: {
|
|
38
|
+
model?: string;
|
|
39
|
+
effort?: string;
|
|
40
|
+
} | null;
|
|
41
|
+
}
|
|
42
|
+
export declare function normalizeMatrixCellAvailability(result: MatrixCellAvailabilityResponse): MatrixCellAvailabilityResult;
|
|
43
|
+
export declare function validateCursorSubagentModel(value: string, options: ValidateMatrixCellOptions): Promise<MatrixCellAvailabilityResult>;
|
|
44
|
+
export declare function validateMatrixCell(provider: string, value: string, options: ValidateMatrixCellOptions): Promise<MatrixCellAvailabilityResponse>;
|
|
45
|
+
//# sourceMappingURL=availability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/availability.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,eAAe,GAAG,aAAa,CAAC;AAE/E,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,sBAAsB,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,8BAA8B,GACtC,sBAAsB,GACtB,4BAA4B,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,qBAAqB,KAC3B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,eAAe,KACrB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;CACV;AAWD,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,8BAA8B,GACrC,4BAA4B,CAE9B;AA+VD,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,4BAA4B,CAAC,CAsEvC;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,8BAA8B,CAAC,CA2CzC"}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { VALID_CLAUDE_DISPATCH_CEILINGS } from '../../config/oat-config.js';
|
|
4
|
+
import { fileExists } from '../../fs/io.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export function normalizeMatrixCellAvailability(result) {
|
|
7
|
+
return typeof result === 'string' ? { availability: result } : result;
|
|
8
|
+
}
|
|
9
|
+
async function runCursorAgent(args, options) {
|
|
10
|
+
try {
|
|
11
|
+
const { stdout, stderr } = await execFileAsync('cursor-agent', args, {
|
|
12
|
+
cwd: options.cwd,
|
|
13
|
+
env: options.env,
|
|
14
|
+
maxBuffer: 1024 * 1024,
|
|
15
|
+
timeout: 10_000,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
stdout: String(stdout),
|
|
20
|
+
stderr: String(stderr),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const failure = error;
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
stdout: String(failure.stdout ?? ''),
|
|
28
|
+
stderr: String(failure.stderr ?? failure.message),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function runCodex(args, options) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout, stderr } = await execFileAsync('codex', args, {
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
env: options.env,
|
|
37
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
38
|
+
timeout: 10_000,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
stdout: String(stdout),
|
|
43
|
+
stderr: String(stderr),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const failure = error;
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
stdout: String(failure.stdout ?? ''),
|
|
51
|
+
stderr: String(failure.stderr ?? failure.message),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const DEFAULT_DEPENDENCIES = {
|
|
56
|
+
pathExists: fileExists,
|
|
57
|
+
runCursorAgent,
|
|
58
|
+
runCodex,
|
|
59
|
+
env: process.env,
|
|
60
|
+
};
|
|
61
|
+
function apiKeyArgs(env) {
|
|
62
|
+
const apiKey = env.CURSOR_API_KEY;
|
|
63
|
+
return typeof apiKey === 'string' && apiKey.length > 0
|
|
64
|
+
? ['--api-key', apiKey]
|
|
65
|
+
: [];
|
|
66
|
+
}
|
|
67
|
+
function parseCursorCatalog(stdout) {
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
70
|
+
const match = line.match(/^(\S+)\s+-\s+(.+)$/);
|
|
71
|
+
if (!match) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const [, slug] = match;
|
|
75
|
+
if (slug) {
|
|
76
|
+
entries.push({ slug });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
function availabilityFromCursorCatalog(value, stdout) {
|
|
82
|
+
const entries = parseCursorCatalog(stdout);
|
|
83
|
+
if (entries.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return entries.some((entry) => entry.slug === value)
|
|
87
|
+
? 'valid'
|
|
88
|
+
: 'unknown-value';
|
|
89
|
+
}
|
|
90
|
+
const CURSOR_SUBAGENT_PROBE_SENTINEL = 'OAT_CURSOR_SUBAGENT_MODEL_VALID';
|
|
91
|
+
function hasCursorSubagentProbeSentinel(stdout) {
|
|
92
|
+
return stdout
|
|
93
|
+
.split(/\r?\n/)
|
|
94
|
+
.some((line) => line.trim() === CURSOR_SUBAGENT_PROBE_SENTINEL);
|
|
95
|
+
}
|
|
96
|
+
function cursorSubagentProbePrompt(model) {
|
|
97
|
+
return [
|
|
98
|
+
'Validate whether a Cursor subagent Task can be launched with a specific model.',
|
|
99
|
+
`Use the Task tool once with model "${model}" and ask the subagent to reply exactly: ${CURSOR_SUBAGENT_PROBE_SENTINEL}.`,
|
|
100
|
+
'After the subagent returns, print only its exact reply.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
function cursorSubagentProbeArgs(model, env) {
|
|
104
|
+
return [
|
|
105
|
+
...apiKeyArgs(env),
|
|
106
|
+
'-p',
|
|
107
|
+
cursorSubagentProbePrompt(model),
|
|
108
|
+
'--output-format=text',
|
|
109
|
+
'--force',
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
function unique(values) {
|
|
113
|
+
return [...new Set(values)];
|
|
114
|
+
}
|
|
115
|
+
function extractModelSlugs(value) {
|
|
116
|
+
const matches = value.match(/\b[a-z][a-z0-9]*(?:[-.][a-z0-9]+)+\b/gi) ?? [];
|
|
117
|
+
return unique(matches
|
|
118
|
+
.map((match) => match.replace(/[),.;:]+$/g, ''))
|
|
119
|
+
.filter((match) => /\d/.test(match)));
|
|
120
|
+
}
|
|
121
|
+
function parseCursorAllowedSubagentModels(output) {
|
|
122
|
+
const sections = [];
|
|
123
|
+
const patterns = [
|
|
124
|
+
/\ballowed\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
|
|
125
|
+
/\bsupported\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
|
|
126
|
+
/\bvalid\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
|
|
127
|
+
/\bone\s+of\s*:?\s*([^\n]+)/gi,
|
|
128
|
+
];
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
for (const match of output.matchAll(pattern)) {
|
|
131
|
+
if (match[1]) {
|
|
132
|
+
sections.push(match[1]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return unique(sections.flatMap(extractModelSlugs));
|
|
137
|
+
}
|
|
138
|
+
function cursorAllowedModelsMessage(allowedValues) {
|
|
139
|
+
return `Allowed subagent models: ${allowedValues.join(', ')}.`;
|
|
140
|
+
}
|
|
141
|
+
function cursorRejectedMessage(allowedValues) {
|
|
142
|
+
return `Cursor rejected this model for subagent Task dispatch. ${cursorAllowedModelsMessage(allowedValues)}`;
|
|
143
|
+
}
|
|
144
|
+
function cursorCatalogContextMessage(value, availability) {
|
|
145
|
+
if (availability === 'valid') {
|
|
146
|
+
return `Cursor's broad model catalog lists '${value}', but subagent Task dispatch could not be validated.`;
|
|
147
|
+
}
|
|
148
|
+
if (availability === 'unknown-value') {
|
|
149
|
+
return `Cursor's broad model catalog does not list '${value}'.`;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
function isRecord(value) {
|
|
154
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
155
|
+
}
|
|
156
|
+
function stringFromUnknown(value) {
|
|
157
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
158
|
+
? value.trim()
|
|
159
|
+
: null;
|
|
160
|
+
}
|
|
161
|
+
function extractCodexEffort(value) {
|
|
162
|
+
if (typeof value === 'string') {
|
|
163
|
+
return stringFromUnknown(value);
|
|
164
|
+
}
|
|
165
|
+
if (!isRecord(value)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return (stringFromUnknown(value.effort) ??
|
|
169
|
+
stringFromUnknown(value.id) ??
|
|
170
|
+
stringFromUnknown(value.name));
|
|
171
|
+
}
|
|
172
|
+
function extractCodexEfforts(model) {
|
|
173
|
+
const efforts = [];
|
|
174
|
+
for (const key of [
|
|
175
|
+
'supported_reasoning_levels',
|
|
176
|
+
'supported_reasoning_efforts',
|
|
177
|
+
'supported_efforts',
|
|
178
|
+
]) {
|
|
179
|
+
const value = model[key];
|
|
180
|
+
if (!Array.isArray(value)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
efforts.push(...value
|
|
184
|
+
.map(extractCodexEffort)
|
|
185
|
+
.filter((effort) => effort !== null));
|
|
186
|
+
}
|
|
187
|
+
const defaultEffort = stringFromUnknown(model.default_reasoning_level);
|
|
188
|
+
if (defaultEffort) {
|
|
189
|
+
efforts.push(defaultEffort);
|
|
190
|
+
}
|
|
191
|
+
return unique(efforts);
|
|
192
|
+
}
|
|
193
|
+
function parseCodexCatalog(stdout) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(stdout);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.models)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const entries = [];
|
|
205
|
+
for (const model of parsed.models) {
|
|
206
|
+
if (!isRecord(model)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const slug = stringFromUnknown(model.slug);
|
|
210
|
+
if (!slug) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const supportedEfforts = extractCodexEfforts(model);
|
|
214
|
+
entries.push({
|
|
215
|
+
slug,
|
|
216
|
+
supportedEfforts: supportedEfforts.length > 0 ? supportedEfforts : null,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return entries.length > 0 ? entries : null;
|
|
220
|
+
}
|
|
221
|
+
function codexTargetFromValue(value, target) {
|
|
222
|
+
const targetModel = stringFromUnknown(target?.model);
|
|
223
|
+
const targetEffort = stringFromUnknown(target?.effort);
|
|
224
|
+
if (targetModel && targetEffort) {
|
|
225
|
+
return { model: targetModel, effort: targetEffort };
|
|
226
|
+
}
|
|
227
|
+
const [model, effort] = value.split('/');
|
|
228
|
+
return {
|
|
229
|
+
model: stringFromUnknown(model),
|
|
230
|
+
effort: stringFromUnknown(effort),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function codexUnsupportedEffortMessage(model, effort, supportedEfforts) {
|
|
234
|
+
return `Codex debug models lists '${model}', but effort '${effort}' is not supported. Supported Codex efforts: ${supportedEfforts.join(', ')}.`;
|
|
235
|
+
}
|
|
236
|
+
async function validateCodexCell(value, cwd, dependencies, target) {
|
|
237
|
+
const { model, effort } = codexTargetFromValue(value, target);
|
|
238
|
+
if (!model || !effort || value.trim().length === 0) {
|
|
239
|
+
return { availability: 'unknown-value' };
|
|
240
|
+
}
|
|
241
|
+
const result = await dependencies.runCodex(['debug', 'models'], {
|
|
242
|
+
cwd,
|
|
243
|
+
env: dependencies.env ?? process.env,
|
|
244
|
+
});
|
|
245
|
+
if (!result.ok) {
|
|
246
|
+
return { availability: 'unvalidated' };
|
|
247
|
+
}
|
|
248
|
+
const catalog = parseCodexCatalog(result.stdout);
|
|
249
|
+
if (!catalog) {
|
|
250
|
+
return { availability: 'unvalidated' };
|
|
251
|
+
}
|
|
252
|
+
const entry = catalog.find((modelEntry) => modelEntry.slug === model);
|
|
253
|
+
if (!entry) {
|
|
254
|
+
return {
|
|
255
|
+
availability: 'unknown-value',
|
|
256
|
+
message: `Codex debug models does not list '${model}'.`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (!entry.supportedEfforts) {
|
|
260
|
+
return {
|
|
261
|
+
availability: 'unvalidated',
|
|
262
|
+
message: `Codex debug models lists '${model}', but supported reasoning efforts could not be parsed.`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (!entry.supportedEfforts.includes(effort)) {
|
|
266
|
+
return {
|
|
267
|
+
availability: 'unknown-value',
|
|
268
|
+
allowedValues: entry.supportedEfforts,
|
|
269
|
+
message: codexUnsupportedEffortMessage(model, effort, entry.supportedEfforts),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { availability: 'valid' };
|
|
273
|
+
}
|
|
274
|
+
export async function validateCursorSubagentModel(value, options) {
|
|
275
|
+
const dependencies = {
|
|
276
|
+
...DEFAULT_DEPENDENCIES,
|
|
277
|
+
env: options.env ?? DEFAULT_DEPENDENCIES.env,
|
|
278
|
+
...options.dependencies,
|
|
279
|
+
};
|
|
280
|
+
const env = dependencies.env ?? process.env;
|
|
281
|
+
const runOptions = { cwd: options.cwd, env };
|
|
282
|
+
const probeResult = await dependencies.runCursorAgent(cursorSubagentProbeArgs(value, env), runOptions);
|
|
283
|
+
const probeOutput = `${probeResult.stdout}\n${probeResult.stderr}`;
|
|
284
|
+
if (probeResult.ok && hasCursorSubagentProbeSentinel(probeResult.stdout)) {
|
|
285
|
+
return { availability: 'valid' };
|
|
286
|
+
}
|
|
287
|
+
const allowedValues = parseCursorAllowedSubagentModels(probeOutput);
|
|
288
|
+
if (allowedValues.length > 0) {
|
|
289
|
+
const availability = allowedValues.includes(value)
|
|
290
|
+
? 'valid'
|
|
291
|
+
: 'unknown-value';
|
|
292
|
+
return {
|
|
293
|
+
availability,
|
|
294
|
+
allowedValues,
|
|
295
|
+
message: availability === 'valid'
|
|
296
|
+
? cursorAllowedModelsMessage(allowedValues)
|
|
297
|
+
: cursorRejectedMessage(allowedValues),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const modelsResult = await dependencies.runCursorAgent([...apiKeyArgs(env), 'models'], runOptions);
|
|
301
|
+
if (modelsResult.ok) {
|
|
302
|
+
const availability = availabilityFromCursorCatalog(value, modelsResult.stdout);
|
|
303
|
+
if (availability !== null) {
|
|
304
|
+
return {
|
|
305
|
+
availability: availability === 'valid' ? 'unvalidated' : 'unknown-value',
|
|
306
|
+
message: cursorCatalogContextMessage(value, availability),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const listResult = await dependencies.runCursorAgent([...apiKeyArgs(env), '--list-models'], runOptions);
|
|
311
|
+
if (listResult.ok) {
|
|
312
|
+
const availability = availabilityFromCursorCatalog(value, listResult.stdout);
|
|
313
|
+
if (availability !== null) {
|
|
314
|
+
return {
|
|
315
|
+
availability: availability === 'valid' ? 'unvalidated' : 'unknown-value',
|
|
316
|
+
message: cursorCatalogContextMessage(value, availability),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { availability: 'unvalidated' };
|
|
321
|
+
}
|
|
322
|
+
export async function validateMatrixCell(provider, value, options) {
|
|
323
|
+
const normalizedProvider = provider.trim().toLowerCase();
|
|
324
|
+
const normalizedValue = value.trim();
|
|
325
|
+
if (!normalizedProvider || !normalizedValue) {
|
|
326
|
+
return options.detailed
|
|
327
|
+
? { availability: 'unknown-value' }
|
|
328
|
+
: 'unknown-value';
|
|
329
|
+
}
|
|
330
|
+
const dependencies = {
|
|
331
|
+
...DEFAULT_DEPENDENCIES,
|
|
332
|
+
env: options.env ?? DEFAULT_DEPENDENCIES.env,
|
|
333
|
+
...options.dependencies,
|
|
334
|
+
};
|
|
335
|
+
if (normalizedProvider === 'claude') {
|
|
336
|
+
const availability = VALID_CLAUDE_DISPATCH_CEILINGS.includes(normalizedValue)
|
|
337
|
+
? 'valid'
|
|
338
|
+
: 'unknown-value';
|
|
339
|
+
return options.detailed ? { availability } : availability;
|
|
340
|
+
}
|
|
341
|
+
if (normalizedProvider === 'codex') {
|
|
342
|
+
const result = await validateCodexCell(normalizedValue, options.cwd, dependencies, options.target);
|
|
343
|
+
return options.detailed ? result : result.availability;
|
|
344
|
+
}
|
|
345
|
+
if (normalizedProvider === 'cursor') {
|
|
346
|
+
const result = await validateCursorSubagentModel(normalizedValue, {
|
|
347
|
+
...options,
|
|
348
|
+
dependencies,
|
|
349
|
+
});
|
|
350
|
+
return options.detailed ? result : result.availability;
|
|
351
|
+
}
|
|
352
|
+
return options.detailed ? { availability: 'unvalidated' } : 'unvalidated';
|
|
353
|
+
}
|