@kbediako/codex-orchestrator 0.1.3 → 0.1.4
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/README.md +6 -1
- package/dist/bin/codex-orchestrator.js +38 -0
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +4 -1
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +217 -40
- package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
- package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
- package/package.json +3 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { appendFile, mkdir, open, rm } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
4
6
|
import { listDirectories, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
|
|
5
7
|
import { acquireLockWithRetry } from './lockFile.js';
|
|
6
8
|
import { sanitizeTaskId } from './sanitizeTaskId.js';
|
|
7
|
-
import { writeAtomicFile } from '../utils/atomicWrite.js';
|
|
8
9
|
export class ExperienceStoreLockError extends Error {
|
|
9
10
|
taskId;
|
|
10
11
|
constructor(message, taskId) {
|
|
@@ -30,7 +31,8 @@ export class ExperienceStore {
|
|
|
30
31
|
maxAttempts: 5,
|
|
31
32
|
initialDelayMs: 100,
|
|
32
33
|
backoffFactor: 2,
|
|
33
|
-
maxDelayMs: 1000
|
|
34
|
+
maxDelayMs: 1000,
|
|
35
|
+
staleMs: 5 * 60 * 1000
|
|
34
36
|
};
|
|
35
37
|
const overrides = options.lockRetry ?? {};
|
|
36
38
|
const sanitizedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
@@ -52,10 +54,10 @@ export class ExperienceStore {
|
|
|
52
54
|
const targetDir = join(this.outDir, taskId);
|
|
53
55
|
await mkdir(targetDir, { recursive: true });
|
|
54
56
|
const filePath = join(targetDir, 'experiences.jsonl');
|
|
55
|
-
const existing = await this.readRecords(filePath);
|
|
56
57
|
const nextRecords = inputs.map((input) => this.prepareRecord(input, manifestPath));
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
await this.ensureTrailingNewline(filePath);
|
|
59
|
+
const payload = nextRecords.map((record) => JSON.stringify(record)).join('\n');
|
|
60
|
+
await appendFile(filePath, `${payload}\n`, 'utf8');
|
|
59
61
|
return nextRecords;
|
|
60
62
|
}
|
|
61
63
|
finally {
|
|
@@ -72,36 +74,26 @@ export class ExperienceStore {
|
|
|
72
74
|
if (limit === 0) {
|
|
73
75
|
return [];
|
|
74
76
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
: await this.readAllRecords();
|
|
78
|
-
const filtered = allRecords.filter((record) => {
|
|
77
|
+
const collector = createTopKCollector(limit, params.minReward);
|
|
78
|
+
const applyRecord = (record) => {
|
|
79
79
|
if (record.domain !== safeDomain) {
|
|
80
|
-
return
|
|
80
|
+
return;
|
|
81
81
|
}
|
|
82
82
|
if (taskFilter && record.taskId !== taskFilter) {
|
|
83
|
-
return
|
|
83
|
+
return;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
return entry.score >= params.minReward;
|
|
97
|
-
})
|
|
98
|
-
.sort((a, b) => {
|
|
99
|
-
if (b.score === a.score) {
|
|
100
|
-
return b.record.createdAt.localeCompare(a.record.createdAt);
|
|
85
|
+
collector.add(record);
|
|
86
|
+
};
|
|
87
|
+
if (taskFilter) {
|
|
88
|
+
await this.scanRecords(join(this.outDir, taskFilter, 'experiences.jsonl'), applyRecord);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const directories = await listDirectories(this.outDir);
|
|
92
|
+
for (const dir of directories) {
|
|
93
|
+
await this.scanRecords(join(this.outDir, dir, 'experiences.jsonl'), applyRecord);
|
|
101
94
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return scored.slice(0, limit).map((entry) => entry.record);
|
|
95
|
+
}
|
|
96
|
+
return collector.finalize();
|
|
105
97
|
}
|
|
106
98
|
verifyStamp(record) {
|
|
107
99
|
return HEX_STAMP_PATTERN.test(record.stampSignature);
|
|
@@ -156,35 +148,110 @@ export class ExperienceStore {
|
|
|
156
148
|
async releaseLock(lockPath) {
|
|
157
149
|
await rm(lockPath, { force: true });
|
|
158
150
|
}
|
|
159
|
-
async
|
|
151
|
+
async scanRecords(filePath, onRecord) {
|
|
160
152
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
|
|
153
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
154
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
155
|
+
for await (const line of reader) {
|
|
156
|
+
const trimmed = line.trim();
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const record = JSON.parse(trimmed);
|
|
162
|
+
onRecord(record);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
166
168
|
}
|
|
167
169
|
catch (error) {
|
|
168
170
|
if (error.code === 'ENOENT') {
|
|
169
|
-
return
|
|
171
|
+
return;
|
|
170
172
|
}
|
|
171
173
|
throw error;
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
|
-
async
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
async ensureTrailingNewline(filePath) {
|
|
177
|
+
try {
|
|
178
|
+
const handle = await open(filePath, 'r');
|
|
179
|
+
let needsNewline = false;
|
|
180
|
+
try {
|
|
181
|
+
const { size } = await handle.stat();
|
|
182
|
+
if (size === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const buffer = Buffer.alloc(1);
|
|
186
|
+
await handle.read(buffer, 0, 1, size - 1);
|
|
187
|
+
needsNewline = buffer[0] !== 0x0a;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await handle.close();
|
|
191
|
+
}
|
|
192
|
+
if (needsNewline) {
|
|
193
|
+
await appendFile(filePath, '\n', 'utf8');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT') {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
182
202
|
}
|
|
183
203
|
generateId() {
|
|
184
204
|
const suffix = randomBytes(3).toString('hex');
|
|
185
205
|
return `exp-${Date.now().toString(36)}-${suffix}`;
|
|
186
206
|
}
|
|
187
207
|
}
|
|
208
|
+
function createTopKCollector(limit, minReward) {
|
|
209
|
+
const entries = [];
|
|
210
|
+
const threshold = typeof minReward === 'number' ? minReward : null;
|
|
211
|
+
const compare = (a, b) => {
|
|
212
|
+
if (a.score !== b.score) {
|
|
213
|
+
return a.score - b.score;
|
|
214
|
+
}
|
|
215
|
+
const aTime = a.record.createdAt ?? '';
|
|
216
|
+
const bTime = b.record.createdAt ?? '';
|
|
217
|
+
return aTime.localeCompare(bTime);
|
|
218
|
+
};
|
|
219
|
+
const add = (record) => {
|
|
220
|
+
const score = record.reward.gtScore + record.reward.relativeRank;
|
|
221
|
+
if (threshold !== null && score < threshold) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const entry = { record, score };
|
|
225
|
+
if (entries.length === 0) {
|
|
226
|
+
entries.push(entry);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const worst = entries[0];
|
|
230
|
+
if (entries.length >= limit && worst && compare(entry, worst) <= 0) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
let index = 0;
|
|
234
|
+
while (index < entries.length && compare(entries[index], entry) <= 0) {
|
|
235
|
+
index += 1;
|
|
236
|
+
}
|
|
237
|
+
entries.splice(index, 0, entry);
|
|
238
|
+
if (entries.length > limit) {
|
|
239
|
+
entries.shift();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const finalize = () => entries
|
|
243
|
+
.slice()
|
|
244
|
+
.sort((a, b) => {
|
|
245
|
+
if (a.score !== b.score) {
|
|
246
|
+
return b.score - a.score;
|
|
247
|
+
}
|
|
248
|
+
const aTime = a.record.createdAt ?? '';
|
|
249
|
+
const bTime = b.record.createdAt ?? '';
|
|
250
|
+
return bTime.localeCompare(aTime);
|
|
251
|
+
})
|
|
252
|
+
.map((entry) => entry.record);
|
|
253
|
+
return { add, finalize };
|
|
254
|
+
}
|
|
188
255
|
function truncateSummary(value, maxWords) {
|
|
189
256
|
const tokens = value.trim().split(/\s+/u).filter(Boolean);
|
|
190
257
|
if (tokens.length <= maxWords) {
|
|
@@ -33,10 +33,12 @@ export class PersistenceCoordinator {
|
|
|
33
33
|
}
|
|
34
34
|
async handleRunCompleted(summary) {
|
|
35
35
|
let stateStoreError = null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const [stateResult, manifestResult] = await Promise.allSettled([
|
|
37
|
+
this.stateStore.recordRun(summary),
|
|
38
|
+
this.manifestWriter.write(summary)
|
|
39
|
+
]);
|
|
40
|
+
if (stateResult.status === 'rejected') {
|
|
41
|
+
const error = stateResult.reason;
|
|
40
42
|
stateStoreError = error;
|
|
41
43
|
if (error instanceof TaskStateStoreLockError) {
|
|
42
44
|
logger.warn(`Task state snapshot skipped for task ${summary.taskId} (run ${summary.runId}): ${error.message}`);
|
|
@@ -45,10 +47,8 @@ export class PersistenceCoordinator {
|
|
|
45
47
|
logger.error(`Task state snapshot failed for task ${summary.taskId} (run ${summary.runId})`, error);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
50
|
+
if (manifestResult.status === 'rejected') {
|
|
51
|
+
const error = manifestResult.reason;
|
|
52
52
|
this.options.onError?.(error, summary);
|
|
53
53
|
if (!this.options.onError) {
|
|
54
54
|
logger.error(`PersistenceCoordinator manifest write error for task ${summary.taskId} (run ${summary.runId})`, error);
|
|
@@ -27,7 +27,8 @@ export class TaskStateStore {
|
|
|
27
27
|
maxAttempts: 5,
|
|
28
28
|
initialDelayMs: 100,
|
|
29
29
|
backoffFactor: 2,
|
|
30
|
-
maxDelayMs: 1000
|
|
30
|
+
maxDelayMs: 1000,
|
|
31
|
+
staleMs: 5 * 60 * 1000
|
|
31
32
|
};
|
|
32
33
|
const overrides = options.lockRetry ?? {};
|
|
33
34
|
const sanitizedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { open } from 'node:fs/promises';
|
|
1
|
+
import { open, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
3
|
export async function acquireLockWithRetry(params) {
|
|
4
4
|
await params.ensureDirectory();
|
|
5
5
|
const { maxAttempts, initialDelayMs, backoffFactor, maxDelayMs } = params.retry;
|
|
6
|
+
const staleMs = params.retry.staleMs ?? 0;
|
|
6
7
|
let attempt = 0;
|
|
7
8
|
let delayMs = initialDelayMs;
|
|
8
9
|
while (attempt < maxAttempts) {
|
|
@@ -16,6 +17,13 @@ export async function acquireLockWithRetry(params) {
|
|
|
16
17
|
if (error.code !== 'EEXIST') {
|
|
17
18
|
throw error;
|
|
18
19
|
}
|
|
20
|
+
if (staleMs > 0) {
|
|
21
|
+
const cleared = await clearStaleLock(params.lockPath, staleMs);
|
|
22
|
+
if (cleared) {
|
|
23
|
+
attempt -= 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
19
27
|
if (attempt >= maxAttempts) {
|
|
20
28
|
throw params.createError(params.taskId, attempt);
|
|
21
29
|
}
|
|
@@ -24,3 +32,20 @@ export async function acquireLockWithRetry(params) {
|
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
}
|
|
35
|
+
async function clearStaleLock(lockPath, staleMs) {
|
|
36
|
+
try {
|
|
37
|
+
const stats = await stat(lockPath);
|
|
38
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
39
|
+
if (!Number.isFinite(ageMs) || ageMs <= staleMs) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
await rm(lockPath, { force: true });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -52,7 +52,7 @@ export class CloudSyncWorker {
|
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
this.options.onError?.(error, summary, 0);
|
|
55
|
-
await this.
|
|
55
|
+
await this.safeAppendAuditLog({
|
|
56
56
|
level: 'error',
|
|
57
57
|
message: 'Failed to read manifest before sync',
|
|
58
58
|
summary,
|
|
@@ -74,7 +74,7 @@ export class CloudSyncWorker {
|
|
|
74
74
|
idempotencyKey
|
|
75
75
|
});
|
|
76
76
|
this.options.onSuccess?.(result, summary);
|
|
77
|
-
await this.
|
|
77
|
+
await this.safeAppendAuditLog({
|
|
78
78
|
level: 'info',
|
|
79
79
|
message: 'Cloud sync completed',
|
|
80
80
|
summary,
|
|
@@ -84,7 +84,7 @@ export class CloudSyncWorker {
|
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
86
86
|
this.options.onError?.(error, summary, attempt);
|
|
87
|
-
await this.
|
|
87
|
+
await this.safeAppendAuditLog({
|
|
88
88
|
level: 'error',
|
|
89
89
|
message: 'Cloud sync attempt failed',
|
|
90
90
|
summary,
|
|
@@ -120,6 +120,19 @@ export class CloudSyncWorker {
|
|
|
120
120
|
});
|
|
121
121
|
await appendFile(logPath, `${line}\n`, 'utf-8');
|
|
122
122
|
}
|
|
123
|
+
async safeAppendAuditLog(entry) {
|
|
124
|
+
try {
|
|
125
|
+
await this.appendAuditLog(entry);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
try {
|
|
129
|
+
this.options.onError?.(error, entry.summary, 0);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// swallow audit log failures
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
123
136
|
shouldRetry(error) {
|
|
124
137
|
if (this.options.retryDecider) {
|
|
125
138
|
return this.options.retryDecider(error);
|
|
@@ -160,7 +173,7 @@ export class CloudSyncWorker {
|
|
|
160
173
|
if (repaired) {
|
|
161
174
|
try {
|
|
162
175
|
const parsed = JSON.parse(repaired);
|
|
163
|
-
await this.
|
|
176
|
+
await this.safeAppendAuditLog({
|
|
164
177
|
level: 'info',
|
|
165
178
|
message: 'Recovered manifest from partial JSON',
|
|
166
179
|
summary,
|
|
@@ -17,6 +17,7 @@ export function createTelemetrySink(options = {}) {
|
|
|
17
17
|
maxFailures: options.maxFailures ?? DEFAULT_MAX_FAILURES,
|
|
18
18
|
backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
|
|
19
19
|
maxBackoffMs: options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS,
|
|
20
|
+
maxQueueSize: options.maxQueueSize,
|
|
20
21
|
fetch: fetchImpl,
|
|
21
22
|
logger: options.logger ?? console
|
|
22
23
|
});
|
|
@@ -43,6 +44,7 @@ class OtelTelemetrySink {
|
|
|
43
44
|
maxFailures;
|
|
44
45
|
backoffMs;
|
|
45
46
|
maxBackoffMs;
|
|
47
|
+
maxQueueSize;
|
|
46
48
|
queue = [];
|
|
47
49
|
disabled = false;
|
|
48
50
|
consecutiveFailures = 0;
|
|
@@ -54,12 +56,14 @@ class OtelTelemetrySink {
|
|
|
54
56
|
this.maxFailures = config.maxFailures;
|
|
55
57
|
this.backoffMs = config.backoffMs;
|
|
56
58
|
this.maxBackoffMs = config.maxBackoffMs;
|
|
59
|
+
this.maxQueueSize = normalizeQueueSize(config.maxQueueSize);
|
|
57
60
|
}
|
|
58
61
|
async record(event) {
|
|
59
62
|
if (this.disabled) {
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
62
65
|
this.queue.push(event);
|
|
66
|
+
this.trimQueue();
|
|
63
67
|
}
|
|
64
68
|
async recordSummary(summary) {
|
|
65
69
|
if (this.disabled) {
|
|
@@ -86,6 +90,7 @@ class OtelTelemetrySink {
|
|
|
86
90
|
}
|
|
87
91
|
catch (error) {
|
|
88
92
|
this.queue.unshift(...batch);
|
|
93
|
+
this.trimQueue();
|
|
89
94
|
this.handleFailure(error);
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -113,6 +118,12 @@ class OtelTelemetrySink {
|
|
|
113
118
|
this.logger.warn(`Telemetry disabled after ${this.consecutiveFailures} consecutive failures: ${String(error)}`);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
121
|
+
trimQueue() {
|
|
122
|
+
if (this.maxQueueSize === null || this.queue.length <= this.maxQueueSize) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.queue.splice(0, this.queue.length - this.maxQueueSize);
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
128
|
function buildSummaryDimensions(metrics) {
|
|
118
129
|
if (!metrics) {
|
|
@@ -140,3 +151,13 @@ async function wait(ms) {
|
|
|
140
151
|
setTimeout(resolve, ms);
|
|
141
152
|
});
|
|
142
153
|
}
|
|
154
|
+
function normalizeQueueSize(value) {
|
|
155
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const normalized = Math.floor(value);
|
|
159
|
+
if (normalized <= 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kbediako/codex-orchestrator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -100,7 +100,9 @@
|
|
|
100
100
|
"esbuild": "^0.25.11"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
|
+
"@iarna/toml": "^2.2.5",
|
|
103
104
|
"ajv": "^8.17.1",
|
|
105
|
+
"canonicalize": "^2.1.0",
|
|
104
106
|
"ink": "^4.4.1",
|
|
105
107
|
"js-yaml": "^4.1.0",
|
|
106
108
|
"react": "^18.3.1"
|