remote-codex 0.11.52 → 0.11.53

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,4 +1,9 @@
1
- import type { Dirent } from 'node:fs';
1
+ import {
2
+ unwatchFile,
3
+ watchFile,
4
+ type Dirent,
5
+ type Stats,
6
+ } from 'node:fs';
2
7
  import fs from 'node:fs/promises';
3
8
  import path from 'node:path';
4
9
 
@@ -10,6 +15,15 @@ import {
10
15
  ThreadTurnDto,
11
16
  truncateAutoThreadTitle,
12
17
  } from '../../shared/src/index';
18
+ import {
19
+ agentTurnToThreadTurnDto,
20
+ codexTurnToAgentTurn,
21
+ } from './historyItems';
22
+ import type {
23
+ CodexTurnError,
24
+ CodexTurnItem,
25
+ CodexTurnStatus,
26
+ } from './types';
13
27
 
14
28
  interface LocalStateThreadRow {
15
29
  id: string;
@@ -25,6 +39,27 @@ interface ParsedTranscript {
25
39
  turns: ThreadTurnDto[];
26
40
  }
27
41
 
42
+ interface PaginatedTurnRow {
43
+ turnId: string;
44
+ rolloutOrdinal: number;
45
+ status: string;
46
+ errorJson: string | null;
47
+ startedAt: number | null;
48
+ }
49
+
50
+ interface PaginatedItemRow {
51
+ turnId: string;
52
+ rolloutOrdinal: number;
53
+ createdAtMs: number;
54
+ itemType: string;
55
+ itemJson: string;
56
+ }
57
+
58
+ interface LocalCodexSessionStoreOptions {
59
+ watchIntervalMs?: number;
60
+ watchThrottleMs?: number;
61
+ }
62
+
28
63
  export interface LocalCodexSessionRecord {
29
64
  sessionId: string;
30
65
  cwd: string;
@@ -74,6 +109,110 @@ function createHistoryItemId(turnId: string, prefix: string, index: number) {
74
109
  return `${turnId}-${prefix}-${index}`;
75
110
  }
76
111
 
112
+ function transcriptMessageText(payload: any) {
113
+ if (!Array.isArray(payload?.content)) {
114
+ return null;
115
+ }
116
+
117
+ const text = payload.content
118
+ .filter(
119
+ (content: any) =>
120
+ (content?.type === 'input_text' || content?.type === 'output_text') &&
121
+ typeof content.text === 'string' &&
122
+ content.text.trim(),
123
+ )
124
+ .map((content: any) => content.text)
125
+ .join('\n\n');
126
+
127
+ return text || null;
128
+ }
129
+
130
+ function camelCaseKey(key: string) {
131
+ return key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
132
+ }
133
+
134
+ function camelCaseValue(value: unknown): unknown {
135
+ if (Array.isArray(value)) {
136
+ return value.map(camelCaseValue);
137
+ }
138
+ if (!value || typeof value !== 'object') {
139
+ return value;
140
+ }
141
+ return Object.fromEntries(
142
+ Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
143
+ camelCaseKey(key),
144
+ camelCaseValue(entry),
145
+ ]),
146
+ );
147
+ }
148
+
149
+ function completedRolloutHistoryItem(
150
+ payload: any,
151
+ timestamp: string | null | undefined,
152
+ ) {
153
+ if (!payload?.item || typeof payload.item !== 'object') {
154
+ return null;
155
+ }
156
+ const normalized = camelCaseValue(payload.item) as Record<string, unknown>;
157
+ if (typeof normalized.id !== 'string' || typeof normalized.type !== 'string') {
158
+ return null;
159
+ }
160
+ const normalizedType =
161
+ normalized.type.charAt(0).toLowerCase() + normalized.type.slice(1);
162
+ if (normalizedType === 'userMessage' || normalizedType === 'agentMessage') {
163
+ return null;
164
+ }
165
+ if (normalizedType === 'reasoning') {
166
+ const summaryText = normalized.summaryText;
167
+ normalized.summary = Array.isArray(summaryText)
168
+ ? summaryText.filter((entry): entry is string => typeof entry === 'string')
169
+ : typeof summaryText === 'string' && summaryText.trim()
170
+ ? [summaryText]
171
+ : [];
172
+ const rawContent = normalized.rawContent;
173
+ normalized.text = Array.isArray(rawContent)
174
+ ? rawContent
175
+ .map((entry) =>
176
+ typeof entry === 'string'
177
+ ? entry
178
+ : entry && typeof entry === 'object' && typeof (entry as any).text === 'string'
179
+ ? (entry as any).text
180
+ : '',
181
+ )
182
+ .filter(Boolean)
183
+ .join('\n')
184
+ : '';
185
+ }
186
+
187
+ const createdAtCandidate =
188
+ typeof payload.started_at_ms === 'number'
189
+ ? payload.started_at_ms
190
+ : timestamp ?? null;
191
+ const agentTurn = codexTurnToAgentTurn({
192
+ id: typeof payload.turn_id === 'string' ? payload.turn_id : 'rollout-turn',
193
+ status: 'inProgress',
194
+ error: null,
195
+ items: [
196
+ {
197
+ ...normalized,
198
+ id: normalized.id,
199
+ type: normalizedType,
200
+ createdAt: createdAtCandidate,
201
+ } as unknown as CodexTurnItem,
202
+ ],
203
+ });
204
+ return agentTurn.items[0] ?? null;
205
+ }
206
+
207
+ function appendUniqueTurnItem(turn: MutableTurn, item: ThreadHistoryItemDto) {
208
+ const existingIndex = turn.items.findIndex((entry) => entry.id === item.id);
209
+ if (existingIndex >= 0) {
210
+ turn.items[existingIndex] = item;
211
+ return;
212
+ }
213
+ turn.items.push(item);
214
+ }
215
+
77
216
  function finalizeTurn(turn: MutableTurn | null, turns: ThreadTurnDto[]) {
78
217
  if (!turn || turn.items.length === 0) {
79
218
  return;
@@ -88,7 +227,129 @@ function finalizeTurn(turn: MutableTurn | null, turns: ThreadTurnDto[]) {
88
227
  });
89
228
  }
90
229
 
230
+ function isoTimestampFromEpochSeconds(value: number | null) {
231
+ return value === null || !Number.isFinite(value)
232
+ ? null
233
+ : new Date(value * 1_000).toISOString();
234
+ }
235
+
236
+ function normalizePaginatedTurnStatus(status: string): CodexTurnStatus {
237
+ switch (status) {
238
+ case 'completed':
239
+ case 'interrupted':
240
+ case 'failed':
241
+ case 'inProgress':
242
+ return status;
243
+ default:
244
+ return 'inProgress';
245
+ }
246
+ }
247
+
248
+ function parsePaginatedTurnError(value: string | null): CodexTurnError | null {
249
+ if (!value) {
250
+ return null;
251
+ }
252
+ try {
253
+ const parsed = JSON.parse(value);
254
+ if (
255
+ parsed &&
256
+ typeof parsed === 'object' &&
257
+ typeof (parsed as Record<string, unknown>).message === 'string'
258
+ ) {
259
+ return parsed as CodexTurnError;
260
+ }
261
+ return { message: value };
262
+ } catch {
263
+ return { message: value };
264
+ }
265
+ }
266
+
267
+ function mergeSessionTurns(
268
+ paginatedTurns: ThreadTurnDto[],
269
+ transcriptTurns: ThreadTurnDto[],
270
+ ) {
271
+ if (paginatedTurns.length === 0) {
272
+ return transcriptTurns;
273
+ }
274
+
275
+ const transcriptById = new Map(
276
+ transcriptTurns.map((turn) => [turn.id, turn]),
277
+ );
278
+ const merged = paginatedTurns.map((turn) => {
279
+ const transcriptTurn = transcriptById.get(turn.id);
280
+ if (!transcriptTurn) {
281
+ return turn;
282
+ }
283
+ transcriptById.delete(turn.id);
284
+
285
+ const paginatedItemIds = new Set(turn.items.map((item) => item.id));
286
+ const missingTranscriptItems = transcriptTurn.items.filter(
287
+ (item) => !paginatedItemIds.has(item.id),
288
+ );
289
+ const items = [...turn.items, ...missingTranscriptItems]
290
+ .map((item, index) => ({ item, index }))
291
+ .sort((left, right) => {
292
+ const leftMillis = Date.parse(left.item.createdAt ?? '');
293
+ const rightMillis = Date.parse(right.item.createdAt ?? '');
294
+ if (Number.isFinite(leftMillis) && Number.isFinite(rightMillis)) {
295
+ const delta = leftMillis - rightMillis;
296
+ return delta === 0 ? left.index - right.index : delta;
297
+ }
298
+ if (Number.isFinite(leftMillis)) return -1;
299
+ if (Number.isFinite(rightMillis)) return 1;
300
+ return left.index - right.index;
301
+ })
302
+ .map((entry, transcriptOrder) => ({
303
+ ...entry.item,
304
+ transcriptOrder,
305
+ }));
306
+
307
+ return {
308
+ ...turn,
309
+ startedAt: turn.startedAt ?? transcriptTurn.startedAt,
310
+ status: transcriptTurn.status,
311
+ error: transcriptTurn.error ?? turn.error,
312
+ items,
313
+ };
314
+ });
315
+
316
+ merged.push(...transcriptById.values());
317
+ return merged.sort((left, right) =>
318
+ (left.startedAt ?? '').localeCompare(right.startedAt ?? ''),
319
+ );
320
+ }
321
+
91
322
  function parseTranscript(contents: string): ParsedTranscript {
323
+ const entries = contents
324
+ .split('\n')
325
+ .filter((line) => line.trim())
326
+ .flatMap((line) => {
327
+ try {
328
+ return [JSON.parse(line)];
329
+ } catch {
330
+ return [];
331
+ }
332
+ });
333
+ let transcriptSegmentIndex = -1;
334
+ const indexedEntries = entries.map((entry: any) => {
335
+ if (
336
+ entry.type === 'event_msg' &&
337
+ entry.payload?.type === 'task_started'
338
+ ) {
339
+ transcriptSegmentIndex += 1;
340
+ }
341
+ return { entry, segmentIndex: transcriptSegmentIndex };
342
+ });
343
+ const legacyMessageSegments = new Set(
344
+ indexedEntries
345
+ .filter(
346
+ ({ entry }) =>
347
+ entry.type === 'event_msg' &&
348
+ (entry.payload?.type === 'user_message' ||
349
+ entry.payload?.type === 'agent_message'),
350
+ )
351
+ .map(({ segmentIndex }) => segmentIndex),
352
+ );
92
353
  const turns: ThreadTurnDto[] = [];
93
354
  let cwd: string | null = null;
94
355
  let currentTurn: MutableTurn | null = null;
@@ -114,18 +375,7 @@ function parseTranscript(contents: string): ParsedTranscript {
114
375
  return currentTurn;
115
376
  };
116
377
 
117
- for (const line of contents.split('\n')) {
118
- if (!line.trim()) {
119
- continue;
120
- }
121
-
122
- let entry: any;
123
- try {
124
- entry = JSON.parse(line);
125
- } catch {
126
- continue;
127
- }
128
-
378
+ for (const { entry, segmentIndex } of indexedEntries) {
129
379
  if (entry.type === 'session_meta') {
130
380
  const payload = entry.payload ?? {};
131
381
  if (typeof payload.cwd === 'string' && payload.cwd.trim()) {
@@ -134,6 +384,45 @@ function parseTranscript(contents: string): ParsedTranscript {
134
384
  continue;
135
385
  }
136
386
 
387
+ if (
388
+ !legacyMessageSegments.has(segmentIndex) &&
389
+ entry.type === 'response_item' &&
390
+ entry.payload?.type === 'message'
391
+ ) {
392
+ const payload = entry.payload;
393
+ const text = transcriptMessageText(payload);
394
+ if (!text || (payload.role !== 'user' && payload.role !== 'assistant')) {
395
+ continue;
396
+ }
397
+
398
+ const turn = ensureCurrentTurn(entry.timestamp);
399
+ if (payload.role === 'user') {
400
+ userItemCount += 1;
401
+ turn.items.push({
402
+ id:
403
+ typeof payload.id === 'string' && payload.id.trim()
404
+ ? payload.id
405
+ : createHistoryItemId(turn.id, 'user', userItemCount),
406
+ kind: 'userMessage',
407
+ text,
408
+ createdAt: entry.timestamp ?? null,
409
+ });
410
+ } else {
411
+ agentItemCount += 1;
412
+ turn.items.push({
413
+ id:
414
+ typeof payload.id === 'string' && payload.id.trim()
415
+ ? payload.id
416
+ : createHistoryItemId(turn.id, 'agent', agentItemCount),
417
+ kind: 'agentMessage',
418
+ text,
419
+ status: typeof payload.phase === 'string' ? payload.phase : null,
420
+ createdAt: entry.timestamp ?? null,
421
+ });
422
+ }
423
+ continue;
424
+ }
425
+
137
426
  if (entry.type !== 'event_msg') {
138
427
  continue;
139
428
  }
@@ -165,6 +454,7 @@ function parseTranscript(contents: string): ParsedTranscript {
165
454
  id: createHistoryItemId(turn.id, 'user', userItemCount),
166
455
  kind: 'userMessage',
167
456
  text: payload.message,
457
+ createdAt: entry.timestamp ?? null,
168
458
  });
169
459
  continue;
170
460
  }
@@ -177,10 +467,22 @@ function parseTranscript(contents: string): ParsedTranscript {
177
467
  kind: 'agentMessage',
178
468
  text: payload.message,
179
469
  status: typeof payload.phase === 'string' ? payload.phase : null,
470
+ createdAt: entry.timestamp ?? null,
180
471
  });
181
472
  continue;
182
473
  }
183
474
 
475
+ if (payloadType === 'item_completed') {
476
+ const item = completedRolloutHistoryItem(
477
+ payload,
478
+ typeof entry.timestamp === 'string' ? entry.timestamp : null,
479
+ );
480
+ if (item) {
481
+ appendUniqueTurnItem(ensureCurrentTurn(entry.timestamp), item);
482
+ }
483
+ continue;
484
+ }
485
+
184
486
  if (payloadType === 'task_complete') {
185
487
  const turn = ensureCurrentTurn(entry.timestamp);
186
488
  turn.status = turn.error ? 'failed' : 'completed';
@@ -218,7 +520,16 @@ async function fileExists(filePath: string) {
218
520
  }
219
521
 
220
522
  export class LocalCodexSessionStore {
221
- constructor(private readonly codexHome: string) {}
523
+ private readonly watchIntervalMs: number;
524
+ private readonly watchThrottleMs: number;
525
+
526
+ constructor(
527
+ private readonly codexHome: string,
528
+ options: LocalCodexSessionStoreOptions = {},
529
+ ) {
530
+ this.watchIntervalMs = options.watchIntervalMs ?? 500;
531
+ this.watchThrottleMs = options.watchThrottleMs ?? 1_500;
532
+ }
222
533
 
223
534
  async findSession(
224
535
  sessionId: string,
@@ -231,6 +542,7 @@ export class LocalCodexSessionStore {
231
542
  const transcript = transcriptPath
232
543
  ? parseTranscript(await fs.readFile(transcriptPath, 'utf8'))
233
544
  : null;
545
+ const paginatedTurns = await this.findPaginatedTurns(sessionId);
234
546
  const cwd = stateRecord?.cwd ?? transcript?.cwd ?? null;
235
547
 
236
548
  if (!cwd) {
@@ -246,7 +558,65 @@ export class LocalCodexSessionStore {
246
558
  basenameFromPath(cwd),
247
559
  model: stateRecord?.model ?? null,
248
560
  rolloutPath: transcriptPath,
249
- turns: transcript?.turns ?? [],
561
+ turns: mergeSessionTurns(paginatedTurns, transcript?.turns ?? []),
562
+ };
563
+ }
564
+
565
+ async watchSession(
566
+ sessionId: string,
567
+ onChange: () => void,
568
+ ): Promise<() => void> {
569
+ const stateRecord = await this.findSessionInStateDatabases(sessionId);
570
+ const transcriptPath = await this.resolveTranscriptPath(
571
+ stateRecord?.rolloutPath ?? null,
572
+ sessionId,
573
+ );
574
+ if (!transcriptPath) {
575
+ return () => {};
576
+ }
577
+
578
+ let stopped = false;
579
+ let lastEmittedAt = 0;
580
+ let pending: NodeJS.Timeout | null = null;
581
+ const emit = () => {
582
+ pending = null;
583
+ if (stopped) {
584
+ return;
585
+ }
586
+ lastEmittedAt = Date.now();
587
+ onChange();
588
+ };
589
+ const schedule = () => {
590
+ if (stopped || pending) {
591
+ return;
592
+ }
593
+ const delay = Math.max(
594
+ 0,
595
+ this.watchThrottleMs - (Date.now() - lastEmittedAt),
596
+ );
597
+ pending = setTimeout(emit, delay);
598
+ };
599
+ const listener = (current: Stats, previous: Stats) => {
600
+ if (
601
+ current.size !== previous.size ||
602
+ current.mtimeMs !== previous.mtimeMs
603
+ ) {
604
+ schedule();
605
+ }
606
+ };
607
+
608
+ watchFile(
609
+ transcriptPath,
610
+ { persistent: false, interval: this.watchIntervalMs },
611
+ listener,
612
+ );
613
+ return () => {
614
+ stopped = true;
615
+ if (pending) {
616
+ clearTimeout(pending);
617
+ pending = null;
618
+ }
619
+ unwatchFile(transcriptPath, listener);
250
620
  };
251
621
  }
252
622
 
@@ -283,35 +653,44 @@ export class LocalCodexSessionStore {
283
653
  private async findSessionInStateDatabases(
284
654
  sessionId: string,
285
655
  ): Promise<LocalStateThreadRow | null> {
286
- let entries: string[];
287
- try {
288
- entries = await fs.readdir(this.codexHome);
289
- } catch {
290
- return null;
291
- }
292
-
293
- const stateFiles = await Promise.all(
294
- entries
295
- .filter((entry) => /^state_\d+\.sqlite$/i.test(entry))
296
- .map(async (entry) => {
297
- const absPath = path.join(this.codexHome, entry);
298
- const stats = await fs.stat(absPath);
299
- return {
300
- absPath,
301
- mtimeMs: stats.mtimeMs,
302
- };
303
- }),
304
- );
656
+ const stateFiles = (
657
+ await Promise.all(
658
+ [this.codexHome, path.join(this.codexHome, 'sqlite')].map(
659
+ async (directory) => {
660
+ let entries: string[];
661
+ try {
662
+ entries = await fs.readdir(directory);
663
+ } catch {
664
+ return [];
665
+ }
666
+
667
+ return Promise.all(
668
+ entries
669
+ .filter((entry) => /^state_\d+\.sqlite$/i.test(entry))
670
+ .map(async (entry) => {
671
+ const absPath = path.join(directory, entry);
672
+ const stats = await fs.stat(absPath);
673
+ return {
674
+ absPath,
675
+ mtimeMs: stats.mtimeMs,
676
+ };
677
+ }),
678
+ );
679
+ },
680
+ ),
681
+ )
682
+ ).flat();
305
683
 
306
684
  stateFiles.sort((left, right) => right.mtimeMs - left.mtimeMs);
307
685
 
308
686
  for (const stateFile of stateFiles) {
309
- const sqlite = new Database(stateFile.absPath, {
310
- readonly: true,
311
- fileMustExist: true,
312
- });
687
+ let sqlite: Database.Database | null = null;
313
688
 
314
689
  try {
690
+ sqlite = new Database(stateFile.absPath, {
691
+ readonly: true,
692
+ fileMustExist: true,
693
+ });
315
694
  const row = sqlite
316
695
  .prepare(
317
696
  `
@@ -332,15 +711,101 @@ export class LocalCodexSessionStore {
332
711
  return row;
333
712
  }
334
713
  } catch {
335
- // Ignore incompatible sqlite files and continue probing.
714
+ // A corrupt or incompatible index must not block rollout-file recovery.
336
715
  } finally {
337
- sqlite.close();
716
+ sqlite?.close();
338
717
  }
339
718
  }
340
719
 
341
720
  return null;
342
721
  }
343
722
 
723
+ private async findPaginatedTurns(sessionId: string) {
724
+ const databasePath = path.join(this.codexHome, 'thread_history_1.sqlite');
725
+ let sqlite: Database.Database | null = null;
726
+ try {
727
+ sqlite = new Database(databasePath, {
728
+ readonly: true,
729
+ fileMustExist: true,
730
+ });
731
+ const turnRows = sqlite.prepare(
732
+ `
733
+ SELECT
734
+ turn_id AS turnId,
735
+ rollout_ordinal AS rolloutOrdinal,
736
+ status,
737
+ error_json AS errorJson,
738
+ started_at AS startedAt
739
+ FROM thread_turns
740
+ WHERE thread_id = ?
741
+ ORDER BY rollout_ordinal ASC
742
+ `,
743
+ ).all(sessionId) as PaginatedTurnRow[];
744
+ if (turnRows.length === 0) {
745
+ return [];
746
+ }
747
+
748
+ const itemRows = sqlite.prepare(
749
+ `
750
+ SELECT
751
+ turn_id AS turnId,
752
+ rollout_ordinal AS rolloutOrdinal,
753
+ created_at_ms AS createdAtMs,
754
+ item_type AS itemType,
755
+ item_json AS itemJson
756
+ FROM thread_items
757
+ WHERE thread_id = ?
758
+ ORDER BY rollout_ordinal ASC
759
+ `,
760
+ ).all(sessionId) as PaginatedItemRow[];
761
+ const itemsByTurnId = new Map<string, CodexTurnItem[]>();
762
+ for (const row of itemRows) {
763
+ let parsed: unknown;
764
+ try {
765
+ parsed = JSON.parse(row.itemJson);
766
+ } catch {
767
+ continue;
768
+ }
769
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
770
+ continue;
771
+ }
772
+ const parsedRecord = parsed as Record<string, unknown>;
773
+ if (typeof parsedRecord.id !== 'string' || !parsedRecord.id.trim()) {
774
+ continue;
775
+ }
776
+ const item = {
777
+ ...parsedRecord,
778
+ id: parsedRecord.id,
779
+ type:
780
+ typeof parsedRecord.type === 'string'
781
+ ? parsedRecord.type
782
+ : row.itemType,
783
+ createdAt: row.createdAtMs,
784
+ } as unknown as CodexTurnItem;
785
+ const turnItems = itemsByTurnId.get(row.turnId) ?? [];
786
+ turnItems.push(item);
787
+ itemsByTurnId.set(row.turnId, turnItems);
788
+ }
789
+
790
+ return turnRows.map((row) => {
791
+ const agentTurn = codexTurnToAgentTurn({
792
+ id: row.turnId,
793
+ status: normalizePaginatedTurnStatus(row.status),
794
+ error: parsePaginatedTurnError(row.errorJson),
795
+ items: itemsByTurnId.get(row.turnId) ?? [],
796
+ });
797
+ return agentTurnToThreadTurnDto({
798
+ ...agentTurn,
799
+ startedAt: isoTimestampFromEpochSeconds(row.startedAt),
800
+ });
801
+ });
802
+ } catch {
803
+ return [];
804
+ } finally {
805
+ sqlite?.close();
806
+ }
807
+ }
808
+
344
809
  private async resolveTranscriptPath(
345
810
  rolloutPath: string | null,
346
811
  sessionId: string,
@@ -8,18 +8,11 @@ param(
8
8
  $ErrorActionPreference = 'Stop'
9
9
  $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
10
10
  $projectPath = Join-Path $repositoryRoot 'apps\windows-device-manager\RemoteCodex.DeviceManager.csproj'
11
- $productManifestPath = Join-Path $repositoryRoot 'apps\windows-device-manager\ProductManifest.cs'
12
11
  $dotnet = (Get-Command dotnet.exe -ErrorAction SilentlyContinue)
13
12
  if (-not $dotnet) {
14
13
  $dotnet = (Get-Command dotnet -ErrorAction Stop)
15
14
  }
16
15
 
17
- $packageVersion = (Get-Content -LiteralPath (Join-Path $repositoryRoot 'package.json') -Raw | ConvertFrom-Json).version
18
- $productManifest = Get-Content -LiteralPath $productManifestPath -Raw
19
- if ($productManifest -notmatch ('RemoteCodexVersion\s*=\s*"{0}"' -f [Regex]::Escape($packageVersion))) {
20
- throw "ProductManifest.RemoteCodexVersion must match package.json version $packageVersion."
21
- }
22
-
23
16
  & $dotnet.Source publish $projectPath `
24
17
  --configuration $Configuration `
25
18
  --runtime $Runtime `