borgmcp 3.9.0 → 3.11.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.
- package/SECURITY.md +1 -1
- package/dist/assimilate-cmd.d.ts +14 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +19 -12
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +25 -3
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +33 -6
- package/dist/cli-help.js.map +1 -1
- package/dist/clone-cmd.d.ts +28 -0
- package/dist/clone-cmd.d.ts.map +1 -0
- package/dist/clone-cmd.js +227 -0
- package/dist/clone-cmd.js.map +1 -0
- package/dist/clone-security.d.ts +9 -0
- package/dist/clone-security.d.ts.map +1 -0
- package/dist/clone-security.js +41 -0
- package/dist/clone-security.js.map +1 -0
- package/dist/cubes.js +2 -2
- package/dist/cubes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.d.ts +11 -0
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +24 -2
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-stream.d.ts +6 -2
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +36 -11
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +7 -2
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +291 -64
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +12 -2
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-clone-args.d.ts +17 -0
- package/dist/parse-clone-args.d.ts.map +1 -0
- package/dist/parse-clone-args.js +38 -0
- package/dist/parse-clone-args.js.map +1 -0
- package/dist/parse-quickstart-args.d.ts +18 -0
- package/dist/parse-quickstart-args.d.ts.map +1 -0
- package/dist/parse-quickstart-args.js +43 -0
- package/dist/parse-quickstart-args.js.map +1 -0
- package/dist/quickstart-cmd.d.ts +23 -0
- package/dist/quickstart-cmd.d.ts.map +1 -0
- package/dist/quickstart-cmd.js +357 -0
- package/dist/quickstart-cmd.js.map +1 -0
- package/dist/remote-client.d.ts +13 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +25 -0
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-commands.js +12 -12
- package/dist/seat-commands.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +1 -0
- package/dist/stream-status.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +3 -1
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/RELEASING.md +94 -23
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +35 -12
- package/src/claude.ts +25 -3
- package/src/cli-help.ts +39 -6
- package/src/clone-cmd.ts +243 -0
- package/src/clone-security.ts +40 -0
- package/src/cubes.ts +2 -2
- package/src/index.ts +4 -1
- package/src/launch-all-cmd.ts +37 -2
- package/src/log-stream.ts +49 -12
- package/src/opencode-drone.ts +328 -65
- package/src/opencode-plugin.ts +13 -1
- package/src/parse-clone-args.ts +44 -0
- package/src/parse-quickstart-args.ts +54 -0
- package/src/quickstart-cmd.ts +396 -0
- package/src/remote-client.ts +36 -0
- package/src/seat-commands.ts +12 -12
- package/src/stream-status.ts +3 -0
- package/src/unknown-subcommand.ts +3 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Redaction and fail-closed credential detection for untrusted Git remotes. */
|
|
2
|
+
|
|
3
|
+
const URL_USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/)([^/\s@]+)@/gi;
|
|
4
|
+
const URL_SUFFIX_RE = /([a-z][a-z0-9+.-]*:\/\/[^\s?#)]+)([?#])[^\s)]*/gi;
|
|
5
|
+
const NAMED_SECRET_RE = /([?&](?:access[_-]?token|api[_-]?key|auth(?:entication)?|credential|password|passwd|private[_-]?key|secret|token)=)[^&#\s)]+/gi;
|
|
6
|
+
const NAMED_SECRET_TEST_RE = new RegExp(NAMED_SECRET_RE.source, 'i');
|
|
7
|
+
const OPTION_SECRET_RE = /((?:^|[\s("'`])--(?:access[_-]?token|api[_-]?key|auth(?:entication)?|credential|password|passwd|private[_-]?key|secret|token)=)[^\s)]*/gi;
|
|
8
|
+
const SCP_REMOTE_RE = /^[^@\s/:]+@[^:\s]+:[^\s]+$/;
|
|
9
|
+
|
|
10
|
+
export function redactCloneSecrets(value: string): string {
|
|
11
|
+
return value
|
|
12
|
+
.replace(URL_USERINFO_RE, '$1<credentials>@')
|
|
13
|
+
.replace(URL_SUFFIX_RE, '$1$2<redacted>')
|
|
14
|
+
.replace(NAMED_SECRET_RE, '$1<redacted>')
|
|
15
|
+
.replace(OPTION_SECRET_RE, '$1<redacted>')
|
|
16
|
+
.replace(/(^|[\s("'`])[^/\s:@]+:[^@\s/]+@(?=[^\s/])/g, '$1<credentials>@');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True means the value must never reach subprocess argv, output, or Git config.
|
|
21
|
+
* Opaque `scheme:...@...` forms fail closed; URL() accepts those but exposes no
|
|
22
|
+
* username/password fields, which is precisely the class that escaped the old flow.
|
|
23
|
+
*/
|
|
24
|
+
export function hasCloneCredentials(value: string): boolean {
|
|
25
|
+
if (NAMED_SECRET_TEST_RE.test(value)) return true;
|
|
26
|
+
if (SCP_REMOTE_RE.test(value)) return false;
|
|
27
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1].toLowerCase();
|
|
28
|
+
if (scheme && !value.toLowerCase().startsWith(`${scheme}://`) && scheme !== 'file') {
|
|
29
|
+
return value.includes('@');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(value);
|
|
33
|
+
if (parsed.protocol === 'ssh:' || parsed.protocol === 'git+ssh:') {
|
|
34
|
+
return parsed.password.length > 0;
|
|
35
|
+
}
|
|
36
|
+
return parsed.username.length > 0 || parsed.password.length > 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return /[^/\s:@]+:[^@\s/]+@/.test(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/cubes.ts
CHANGED
|
@@ -91,8 +91,8 @@ export class LaunchSeatIdentityChangedError extends Error {
|
|
|
91
91
|
|
|
92
92
|
constructor(droneLabel: string) {
|
|
93
93
|
super(
|
|
94
|
-
`borg launch: did not launch '${droneLabel}' — its
|
|
95
|
-
'Run `borg
|
|
94
|
+
`borg launch: did not launch '${droneLabel}' — its registration changed before the launch could start. ` +
|
|
95
|
+
'Run `borg drones` to see the current state, then try again.',
|
|
96
96
|
);
|
|
97
97
|
this.name = 'LaunchSeatIdentityChangedError';
|
|
98
98
|
}
|
package/src/index.ts
CHANGED
|
@@ -133,6 +133,7 @@ import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
|
133
133
|
import {
|
|
134
134
|
connectOpenCodeDrone,
|
|
135
135
|
injectOpenCodeEntry,
|
|
136
|
+
settleOpenCodeEntry,
|
|
136
137
|
configuredOpenCodePort,
|
|
137
138
|
OPEN_CODE_PORT_MISSING_DIAGNOSTIC,
|
|
138
139
|
openCodeLaunchBinding,
|
|
@@ -314,7 +315,9 @@ export async function main() {
|
|
|
314
315
|
installBorgPlugin();
|
|
315
316
|
const active = await getActiveCube();
|
|
316
317
|
if (active && openCodeRuntime) {
|
|
317
|
-
if (await connectOpenCodeRuntime(active))
|
|
318
|
+
if (await connectOpenCodeRuntime(active)) {
|
|
319
|
+
setModuleInjectOpenCode(injectOpenCodeEntry, settleOpenCodeEntry);
|
|
320
|
+
}
|
|
318
321
|
}
|
|
319
322
|
},
|
|
320
323
|
};
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { runPastelistBackend } from './backends/launch-all-pastelist.js';
|
|
|
16
16
|
|
|
17
17
|
type Backend = 'tmux' | 'terminals' | 'pastelist';
|
|
18
18
|
|
|
19
|
+
/** Internal strict-seam result: requested sessions were not dispatched. */
|
|
20
|
+
export const LAUNCH_ALL_NO_DISPATCH_EXIT_CODE = 2;
|
|
21
|
+
|
|
19
22
|
const TMUX_INSTALL_HINT =
|
|
20
23
|
'borg launch-all: tmux not found.\n' +
|
|
21
24
|
' macOS: brew install tmux\n' +
|
|
@@ -239,6 +242,12 @@ export interface RunLaunchAllOptions {
|
|
|
239
242
|
sleep?: (ms: number) => Promise<void>;
|
|
240
243
|
nowISO?: () => string;
|
|
241
244
|
borgPath?: string;
|
|
245
|
+
/** Internal quickstart filter: launch only the requested staffed roster. */
|
|
246
|
+
droneIds?: readonly string[];
|
|
247
|
+
/** Fail when any requested registered drone cannot be discovered or launched. */
|
|
248
|
+
requireAllRequested?: boolean;
|
|
249
|
+
/** Internal resolved target avoids ambiguous same-name lookup during composition. */
|
|
250
|
+
targetCube?: { cubeId: string; name: string };
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
export async function runLaunchAll(
|
|
@@ -253,7 +262,7 @@ export async function runLaunchAll(
|
|
|
253
262
|
const launchDelayMs = resolveLaunchDelayMs(args.flags.launchDelayMs, deps.getEnv('BORG_LAUNCH_DELAY_MS'));
|
|
254
263
|
|
|
255
264
|
// 1. resolve target cube
|
|
256
|
-
const resolved = await resolveTargetCube(args, deps);
|
|
265
|
+
const resolved = opts.targetCube ?? await resolveTargetCube(args, deps);
|
|
257
266
|
if ('error' in resolved) {
|
|
258
267
|
deps.stderr(`borg launch-all: ${resolved.error}\n`);
|
|
259
268
|
return 1;
|
|
@@ -264,7 +273,18 @@ export async function runLaunchAll(
|
|
|
264
273
|
sweepStaleLocks(deps, cubeId, now());
|
|
265
274
|
|
|
266
275
|
// 3. discover candidates
|
|
267
|
-
|
|
276
|
+
let discovered = await discoverDroneCandidates({ targetCubeId: cubeId, only: args.flags.only }, deps);
|
|
277
|
+
if (opts.droneIds !== undefined) {
|
|
278
|
+
const requested = new Set(opts.droneIds);
|
|
279
|
+
discovered = discovered.filter((candidate) => requested.has(candidate.droneId));
|
|
280
|
+
if (opts.requireAllRequested && discovered.length !== requested.size) {
|
|
281
|
+
deps.stderr(
|
|
282
|
+
`borg launch-all: found ${discovered.length} of ${requested.size} requested registered drone worktrees; ` +
|
|
283
|
+
'nothing was launched. Run `borg drones` to inspect the missing local registration.\n',
|
|
284
|
+
);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
268
288
|
if (discovered.length === 0) {
|
|
269
289
|
if (args.flags.only !== undefined) {
|
|
270
290
|
deps.stdout(`No worktrees matched --only '${args.flags.only}' for cube '${cubeName}'\n`);
|
|
@@ -392,6 +412,14 @@ export async function runLaunchAll(
|
|
|
392
412
|
}
|
|
393
413
|
launchable.push(c);
|
|
394
414
|
}
|
|
415
|
+
const terminalSkipCount = evictedCount + revokedCount + rejectedCount + trustMismatchCount + credentialRejectedCount;
|
|
416
|
+
if (opts.requireAllRequested && terminalSkipCount > 0) {
|
|
417
|
+
deps.stderr(
|
|
418
|
+
`borg launch-all: ${terminalSkipCount} requested registered drone(s) are not launchable; ` +
|
|
419
|
+
'nothing was launched. Resolve the cause reported above, then retry.\n',
|
|
420
|
+
);
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
395
423
|
if (launchable.length === 0) {
|
|
396
424
|
// Accurate cause counts — an all-rejected sweep must NOT claim "evicted"
|
|
397
425
|
// (and vice-versa). Only name a cause when at least one seat hit it.
|
|
@@ -441,6 +469,13 @@ export async function runLaunchAll(
|
|
|
441
469
|
deps.stderr(sel.hardFail);
|
|
442
470
|
return 1;
|
|
443
471
|
}
|
|
472
|
+
if (sel.backend === 'pastelist' && opts.requireAllRequested) {
|
|
473
|
+
deps.stderr(
|
|
474
|
+
'borg launch-all: pastelist mode prints commands for manual use but does not launch sessions; ' +
|
|
475
|
+
'nothing was launched. Run `borg launch-all` separately to print the commands.\n',
|
|
476
|
+
);
|
|
477
|
+
return LAUNCH_ALL_NO_DISPATCH_EXIT_CODE;
|
|
478
|
+
}
|
|
444
479
|
const sessionName = sanitizeSessionName(cubeName);
|
|
445
480
|
const launchStartISO = nowISO();
|
|
446
481
|
|
package/src/log-stream.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { formatCubeActivityWakeMessage } from './cube-activity-wake-copy.js';
|
|
|
59
59
|
import { readBoundedResponseBody } from './server-response.js';
|
|
60
60
|
import { BorgServerError } from './server-errors.js';
|
|
61
61
|
import { markSeatRejected } from './seats.js';
|
|
62
|
+
import { hasPendingWakeEntry as hasPendingDurableWakeEntry } from './remote-client.js';
|
|
62
63
|
import {
|
|
63
64
|
acquireStreamLease,
|
|
64
65
|
readOwnershipSnapshot,
|
|
@@ -124,13 +125,16 @@ function isProcessAlive(pid: number): boolean {
|
|
|
124
125
|
* Used by defaultDeps when no explicit injectOpenCode is supplied.
|
|
125
126
|
*/
|
|
126
127
|
let _moduleInjectOpenCode:
|
|
127
|
-
| ((text: string, entryId: string, allowSubmit: boolean) => Promise<boolean>)
|
|
128
|
+
| ((text: string, entryId: string, allowSubmit: boolean, sourceEntryId?: string) => Promise<boolean>)
|
|
128
129
|
| undefined;
|
|
130
|
+
let _moduleSettleOpenCodeEntry: ((sourceEntryId: string) => void) | undefined;
|
|
129
131
|
|
|
130
132
|
export function setModuleInjectOpenCode(
|
|
131
|
-
fn: (text: string, entryId: string, allowSubmit: boolean) => Promise<boolean
|
|
133
|
+
fn: (text: string, entryId: string, allowSubmit: boolean, sourceEntryId?: string) => Promise<boolean>,
|
|
134
|
+
settle: (sourceEntryId: string) => void,
|
|
132
135
|
): void {
|
|
133
136
|
_moduleInjectOpenCode = fn;
|
|
137
|
+
_moduleSettleOpenCodeEntry = settle;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
/**
|
|
@@ -403,8 +407,13 @@ export interface StreamDeps {
|
|
|
403
407
|
injectOpenCode?: (
|
|
404
408
|
text: string,
|
|
405
409
|
entryId: string,
|
|
406
|
-
allowSubmit: boolean
|
|
410
|
+
allowSubmit: boolean,
|
|
411
|
+
sourceEntryId?: string,
|
|
407
412
|
) => Promise<boolean>;
|
|
413
|
+
/** Inspect whether one retry source remains beyond the durable unread cursor. */
|
|
414
|
+
hasPendingWakeEntry?: (active: ActiveCube, entryId: string) => Promise<boolean>;
|
|
415
|
+
/** Clear in-process retry diagnostics after the durable entry is consumed. */
|
|
416
|
+
settleOpenCodeEntry?: (sourceEntryId: string) => void;
|
|
408
417
|
}
|
|
409
418
|
|
|
410
419
|
const defaultDeps: Required<StreamDeps> = {
|
|
@@ -420,10 +429,15 @@ const defaultDeps: Required<StreamDeps> = {
|
|
|
420
429
|
abortSignal: new AbortController().signal,
|
|
421
430
|
ownerDeps: {},
|
|
422
431
|
ownerStaleMs: 70_000,
|
|
423
|
-
injectOpenCode: (text, entryId, allowSubmit) =>
|
|
432
|
+
injectOpenCode: (text, entryId, allowSubmit, sourceEntryId) =>
|
|
424
433
|
_moduleInjectOpenCode
|
|
425
|
-
? _moduleInjectOpenCode(text, entryId, allowSubmit)
|
|
434
|
+
? _moduleInjectOpenCode(text, entryId, allowSubmit, sourceEntryId)
|
|
426
435
|
: Promise.resolve(false),
|
|
436
|
+
hasPendingWakeEntry: (active, entryId) => hasPendingDurableWakeEntry(
|
|
437
|
+
active as Parameters<typeof hasPendingDurableWakeEntry>[0],
|
|
438
|
+
entryId,
|
|
439
|
+
),
|
|
440
|
+
settleOpenCodeEntry: (sourceEntryId) => _moduleSettleOpenCodeEntry?.(sourceEntryId),
|
|
427
441
|
};
|
|
428
442
|
|
|
429
443
|
// ------------------------------------------------------------------
|
|
@@ -678,6 +692,8 @@ export async function streamOnce(
|
|
|
678
692
|
hwmDivergenceGraceMs,
|
|
679
693
|
abortSignal,
|
|
680
694
|
injectOpenCode,
|
|
695
|
+
hasPendingWakeEntry,
|
|
696
|
+
settleOpenCodeEntry,
|
|
681
697
|
} = { ...defaultDeps, ...deps };
|
|
682
698
|
|
|
683
699
|
assertUuidShape(active.cubeId, 'cube_id');
|
|
@@ -819,6 +835,17 @@ export async function streamOnce(
|
|
|
819
835
|
pendingHwmDivergence = { hwm, timer };
|
|
820
836
|
};
|
|
821
837
|
|
|
838
|
+
const shouldDeliverWakeRetry = async (entryId: string): Promise<boolean> => {
|
|
839
|
+
try {
|
|
840
|
+
if (await hasPendingWakeEntry(active, entryId)) return true;
|
|
841
|
+
settleOpenCodeEntry(entryId);
|
|
842
|
+
} catch {
|
|
843
|
+
// A retry is never authoritative enough to cross the model boundary on
|
|
844
|
+
// its own. The durable inbox and a later retry remain available.
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
};
|
|
848
|
+
|
|
822
849
|
// Bounded recent-id set for replay-on-reconnect dedup per spec §(3).
|
|
823
850
|
// Set + FIFO array for O(1) membership + bounded memory.
|
|
824
851
|
const recentIds = new Set<string>();
|
|
@@ -844,6 +871,10 @@ export async function streamOnce(
|
|
|
844
871
|
const line = formatInboxLine(withSseEventId(ev.data, ev.id));
|
|
845
872
|
const deliveryId = ev.wake_nonce ?? ev.id;
|
|
846
873
|
const isReping = ev.wake_nonce !== undefined;
|
|
874
|
+
if (isReping && !(await shouldDeliverWakeRetry(ev.id))) {
|
|
875
|
+
markEventPersisted(ev.id, ev.data?.created_at ?? '');
|
|
876
|
+
return 'persisted-skip';
|
|
877
|
+
}
|
|
847
878
|
const eventHwm = ev.data?.created_at
|
|
848
879
|
? { id: ev.id, created_at: ev.data.created_at }
|
|
849
880
|
: null;
|
|
@@ -867,12 +898,11 @@ export async function streamOnce(
|
|
|
867
898
|
await hasInboxEntryId(active.cubeId, active.droneId, ev.id, line)
|
|
868
899
|
);
|
|
869
900
|
if (alreadyPersisted) {
|
|
870
|
-
// An ordinary entry at/before the persisted cursor
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
// underlying inbox line is already durable.
|
|
901
|
+
// An ordinary entry at/before the persisted cursor must not re-enter the
|
|
902
|
+
// OpenCode queue. A still-unread re-ping may reconcile the prior attempt;
|
|
903
|
+
// its source entry ID prevents a new nonce from resubmitting that prompt.
|
|
874
904
|
if (isReping) {
|
|
875
|
-
await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true);
|
|
905
|
+
await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true, ev.id);
|
|
876
906
|
}
|
|
877
907
|
markEventPersisted(ev.id, ev.data?.created_at ?? '');
|
|
878
908
|
return 'persisted-skip';
|
|
@@ -880,7 +910,10 @@ export async function streamOnce(
|
|
|
880
910
|
// The inbox append is the durable record. OpenCode injection is only the
|
|
881
911
|
// wake attempt and may return before the agent finishes processing.
|
|
882
912
|
await appendLine(active.cubeId, active.droneId, line);
|
|
883
|
-
|
|
913
|
+
const openCodeDelivered = isReping
|
|
914
|
+
? await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true, ev.id)
|
|
915
|
+
: await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true);
|
|
916
|
+
if (!openCodeDelivered) {
|
|
884
917
|
if (ev.wake_nonce === undefined) wakeCodex(formatCodexWakePrompt(line));
|
|
885
918
|
else wakeCodex(formatCodexWakePrompt(line), ev.wake_nonce);
|
|
886
919
|
}
|
|
@@ -1198,7 +1231,11 @@ export async function streamOnce(
|
|
|
1198
1231
|
if (isRecentWakeReplay) {
|
|
1199
1232
|
const line = formatInboxLine(withSseEventId(event.data, event.id));
|
|
1200
1233
|
const wakeNonce = event.wake_nonce;
|
|
1201
|
-
if (
|
|
1234
|
+
if (
|
|
1235
|
+
wakeNonce !== undefined &&
|
|
1236
|
+
await shouldDeliverWakeRetry(event.id) &&
|
|
1237
|
+
!(await injectOpenCode(formatOpenCodeWakePrompt(line), wakeNonce, true, event.id))
|
|
1238
|
+
) {
|
|
1202
1239
|
wakeCodex(formatCodexWakePrompt(line), wakeNonce);
|
|
1203
1240
|
}
|
|
1204
1241
|
continue;
|