@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
@@ -5,20 +5,21 @@ import { fileURLToPath } from "node:url";
5
5
  import { URL } from "node:url";
6
6
  import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError } from "../../src/trajectory-contracts.js";
7
7
  import { sameFilesystemPath } from "../../src/paths.js";
8
+ import { TOOL_TIMING_ENTRY_TYPE } from "../../src/tool-timing.js";
8
9
  const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
9
10
 
10
11
  type Socket = import("node:stream").Duplex;
11
12
  type ClientKind = "publisher" | "browser";
12
- type Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; backpressured: boolean; superseded: boolean };
13
+ type RunFocus = { publisherId: string; runId: string };
14
+ type Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; pendingDeliver: (() => void) | undefined; backpressured: boolean; superseded: boolean; focusAware?: boolean; focus?: RunFocus; sentTiming?: Map<string, number> };
13
15
  type State = { type: "state"; publishers: readonly unknown[]; updatedAt: number; initial?: boolean; truncated?: boolean };
14
16
  type PendingRequest = { requestId: string; kind: "transcript" | "action"; browser: Client; publisherId: string; publisher: Client; target: { runId?: string; agentId?: string; subagentId?: string; revision?: number }; timer: ReturnType<typeof setTimeout> };
15
17
  const MAX_FRAME_BYTES = 32 * 1024 * 1024;
16
18
  const MAX_PENDING_REQUESTS = 128;
17
19
  const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
18
20
  const ACTION_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
19
- const TIMING_ENTRY_TYPE = "pi-workflows:tool-timing";
20
21
  function isTimingEntry(value: unknown): boolean {
21
- return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType === TIMING_ENTRY_TYPE);
22
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType === TOOL_TIMING_ENTRY_TYPE);
22
23
  }
23
24
  function compactTranscript(value: unknown): unknown {
24
25
  if (Array.isArray(value)) return value.filter(isTimingEntry);
@@ -39,6 +40,55 @@ function compactSubagent(subagent: unknown): unknown {
39
40
  const record = subagent as { transcript?: unknown };
40
41
  return { ...record, transcript: compactTranscript(record.transcript) };
41
42
  }
43
+ function withoutTiming(run: unknown): unknown {
44
+ if (!run || typeof run !== "object" || Array.isArray(run)) return run;
45
+ const record = run as { transcripts?: unknown };
46
+ if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
47
+ const transcripts: Record<string, unknown> = {};
48
+ for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) transcripts[id] = entry && typeof entry === "object" && !Array.isArray(entry) ? { ...entry, timing: [] } : entry;
49
+ return { ...record, transcripts };
50
+ }
51
+ /** Timing already delivered at this revision is omitted; the browser keeps the entries it cached. */
52
+ function knownTiming(run: unknown, prefix: string, sent: Map<string, number>, mark: (key: string, revision: number) => void): unknown {
53
+ if (!run || typeof run !== "object" || Array.isArray(run)) return run;
54
+ const record = run as { transcripts?: unknown };
55
+ if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
56
+ const transcripts: Record<string, unknown> = {};
57
+ for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) {
58
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) { transcripts[id] = entry; continue; }
59
+ const value = entry as { revision?: unknown; timing?: unknown };
60
+ if (typeof value.revision !== "number" || !Array.isArray(value.timing)) { transcripts[id] = entry; continue; }
61
+ if (sent.get(`${prefix}\t${id}`) === value.revision) { const rest = { ...value }; delete rest.timing; transcripts[id] = rest; continue; }
62
+ mark(`${prefix}\t${id}`, value.revision);
63
+ transcripts[id] = entry;
64
+ }
65
+ return { ...record, transcripts };
66
+ }
67
+ function forgetTiming(run: unknown, prefix: string, sent: Map<string, number>): void {
68
+ if (!run || typeof run !== "object" || Array.isArray(run)) return;
69
+ const record = run as { transcripts?: unknown };
70
+ if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return;
71
+ for (const id of Object.keys(record.transcripts)) sent.delete(`${prefix}\t${id}`);
72
+ }
73
+ /** Tool-timing only drives the focused run's gantt, so every other run ships without it. */
74
+ function focusedState(state: State, focus: RunFocus | undefined, sent?: Map<string, number>, mark: (key: string, revision: number) => void = () => {}): State {
75
+ return {
76
+ ...state,
77
+ publishers: state.publishers.map((publisher) => {
78
+ if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
79
+ const value = publisher as { id?: unknown; runs?: unknown };
80
+ const runs: readonly unknown[] | undefined = Array.isArray(value.runs) ? value.runs as readonly unknown[] : undefined;
81
+ if (!runs) return publisher;
82
+ const focusedPublisher = focus !== undefined && value.id === focus.publisherId;
83
+ return { ...value, runs: runs.map((run) => {
84
+ const id = run && typeof run === "object" && !Array.isArray(run) ? (run as { run?: { id?: unknown } }).run?.id : undefined;
85
+ const prefix = `${String(value.id)}\t${String(id)}`;
86
+ if (!focusedPublisher || id !== focus.runId) { if (sent) forgetTiming(run, prefix, sent); return withoutTiming(run); }
87
+ return sent ? knownTiming(run, prefix, sent, mark) : run;
88
+ }) };
89
+ }),
90
+ };
91
+ }
42
92
  function compactPublishers(publishers: readonly unknown[]): unknown[] {
43
93
  return publishers.map((publisher) => {
44
94
  if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
@@ -77,7 +127,7 @@ function minimalPublisher(publisher: unknown): unknown {
77
127
  function tinyPublisher(publisher: unknown): unknown {
78
128
  if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return { id: "publisher" };
79
129
  const value = publisher as Record<string, unknown>;
80
- return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined, themes: value.themes, connected: value.connected === true };
130
+ return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined, connected: value.connected === true };
81
131
  }
82
132
  function encodePublisherList(publishers: readonly unknown[], updatedAt: number, maxBytes: number, initial: boolean, truncated: boolean, projector: (publisher: unknown) => unknown, fallback?: (publisher: unknown) => unknown): { serialized: string; truncated: boolean } {
83
133
  const prefix = `{"type":"state","publishers":[`;
@@ -133,11 +183,13 @@ function isState(value: unknown): value is State {
133
183
  return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "state");
134
184
  }
135
185
 
136
- function writeFrame(client: Client, packet: Buffer, state: boolean): void {
137
- if (state && client.backpressured) { client.pendingState = packet; return; }
186
+ function writeFrame(client: Client, packet: Buffer, state: boolean, onDelivered?: () => void): void {
187
+ // A superseded pending frame is never sent, so its payload must not count as delivered.
188
+ if (state && client.backpressured) { client.pendingState = packet; client.pendingDeliver = onDelivered; return; }
138
189
  try {
139
- if (state) client.pendingState = undefined;
190
+ if (state) { client.pendingState = undefined; client.pendingDeliver = undefined; }
140
191
  if (!client.socket.write(packet)) client.backpressured = true;
192
+ onDelivered?.();
141
193
  } catch { client.socket.destroy(); }
142
194
  }
143
195
 
@@ -194,6 +246,7 @@ type TrajectoryServerOptions = { maxFrameBytes?: number; fingerprint?: string };
194
246
  export function createTrajectoryServer(port: number, lockPath: string, options: TrajectoryServerOptions = {}): Server {
195
247
  const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
196
248
  const serverFingerprint = options.fingerprint ?? "";
249
+ let startedAt = 0;
197
250
  const clients = new Set<Client>();
198
251
  const publishers = new Map<string, { client: Client; value: Record<string, unknown>; generation: number }>();
199
252
  let latest: State = { type: "state", publishers: [], updatedAt: Date.now(), initial: true };
@@ -234,8 +287,19 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
234
287
  const broadcast = (value: unknown) => {
235
288
  if (isState(value)) {
236
289
  try {
237
- const stateFrame = frame(encodeState(value, maxFrameBytes), maxFrameBytes);
238
- for (const client of clients) if (client.kind === "browser") writeFrame(client, stateFrame, true);
290
+ let shared: Buffer | undefined;
291
+ for (const client of clients) {
292
+ if (client.kind !== "browser") continue;
293
+ if (client.focusAware !== true) {
294
+ shared ??= frame(encodeState(value, maxFrameBytes), maxFrameBytes);
295
+ writeFrame(client, shared, true);
296
+ continue;
297
+ }
298
+ const sent = (client.sentTiming ??= new Map<string, number>());
299
+ const marks: [string, number][] = [];
300
+ const stateFrame = frame(encodeState(focusedState(value, client.focus, sent, (key, revision) => marks.push([key, revision])), maxFrameBytes), maxFrameBytes);
301
+ writeFrame(client, stateFrame, true, () => { for (const [key, revision] of marks) sent.set(key, revision); });
302
+ }
239
303
  } catch {
240
304
  for (const client of clients) if (client.kind === "browser") client.socket.destroy();
241
305
  }
@@ -243,6 +307,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
243
307
  }
244
308
  for (const client of clients) if (client.kind === "browser") emit(client, value);
245
309
  };
310
+ let viewers = 0;
311
+ // Publishers poll their run directory, so they only need a fast cadence while a browser is watching.
312
+ const notifyViewers = () => {
313
+ const count = [...clients].filter((candidate) => candidate.kind === "browser").length;
314
+ if (count === viewers) return;
315
+ viewers = count;
316
+ for (const candidate of clients) if (candidate.kind === "publisher") emit(candidate, { type: "publisher:viewers", count });
317
+ };
246
318
  const publishState = () => {
247
319
  const values = [...publishers.values()].map(({ value, generation }) => ({ ...value, generation }));
248
320
  latest = { type: "state", publishers: values, updatedAt: Date.now() };
@@ -251,6 +323,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
251
323
  const disconnect = (client: Client) => {
252
324
  clearPending(client, client.kind === "publisher" ? "Publisher is disconnected" : "Trajectory browser disconnected");
253
325
  clients.delete(client);
326
+ if (client.kind === "browser") notifyViewers();
254
327
  if (client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) {
255
328
  publishers.delete(client.publisherId);
256
329
  publishState();
@@ -264,7 +337,21 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
264
337
  const message = value as Record<string, unknown>;
265
338
  if (client.superseded) return;
266
339
  if (message.type === "publisher:detach" && client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) { clearPending(client, "Publisher is disconnected"); publishers.delete(client.publisherId); publishState(); scheduleIdleExit(); return; }
267
- if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); return; }
340
+ if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); notifyViewers(); return; }
341
+ if (message.type === "ui:focus" && client.kind === "browser") {
342
+ const publisherId = typeof message.publisherId === "string" ? message.publisherId : undefined;
343
+ const runId = typeof message.runId === "string" ? message.runId : undefined;
344
+ client.focusAware = true;
345
+ client.sentTiming = new Map<string, number>();
346
+ if (publisherId !== undefined && runId !== undefined) client.focus = { publisherId, runId };
347
+ else delete client.focus;
348
+ const sent = client.sentTiming;
349
+ const marks: [string, number][] = [];
350
+ const focused = focusedState(latest, client.focus, sent, (key, revision) => marks.push([key, revision]));
351
+ try { writeFrame(client, frame(encodeState(focused, maxFrameBytes), maxFrameBytes), true, () => { for (const [key, revision] of marks) sent.set(key, revision); }); }
352
+ catch { client.socket.destroy(); }
353
+ return;
354
+ }
268
355
  if (client.kind === "publisher" && message.type === "publisher:attach" && typeof message.publisherId === "string") {
269
356
  cancelIdleExit();
270
357
  client.publisherId = message.publisherId;
@@ -277,6 +364,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
277
364
  }
278
365
  const generation = (previous?.generation ?? 0) + 1;
279
366
  publishers.set(message.publisherId, { client, value: { ...(previous?.value ?? { id: message.publisherId }), id: message.publisherId, connected: true }, generation });
367
+ emit(client, { type: "publisher:viewers", count: viewers });
280
368
  publishState();
281
369
  return;
282
370
  }
@@ -374,7 +462,8 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
374
462
  catch { writeJson(response, 400, { error: "Invalid request" }); return; }
375
463
  if (!authorized(request, port)) { writeJson(response, 403, { error: "Forbidden" }); return; }
376
464
  const path = url.pathname;
377
- if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true }); return; }
465
+ // The identity lets an attaching Pi tell its own server from any other one answering on the port.
466
+ if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true, pid: process.pid, fingerprint: serverFingerprint, startedAt }); return; }
378
467
  if (request.method === "GET" && (path === "/" || path === "/index.html")) {
379
468
  void readFile(new URL("./assets/index.html", import.meta.url)).then((html) => {
380
469
  response.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": html.byteLength, "cache-control": "no-store" });
@@ -396,6 +485,13 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
396
485
  }).catch(() => { writeJson(response, 500, { error: "Trajectory DOM diffing library is unavailable" }); });
397
486
  return;
398
487
  }
488
+ if (request.method === "GET" && path === "/prism.min.js") {
489
+ void readFile(new URL("./assets/prism.min.js", import.meta.url)).then((script) => {
490
+ response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "content-length": script.byteLength, "cache-control": "no-store" });
491
+ response.end(script);
492
+ }).catch(() => { writeJson(response, 500, { error: "Trajectory syntax highlighting library is unavailable" }); });
493
+ return;
494
+ }
399
495
  if (request.method === "GET" && (path === "/favicon.png" || path === "/favicon.ico")) {
400
496
  void readFile(new URL("./assets/favicon.png", import.meta.url)).then((icon) => {
401
497
  response.writeHead(200, { "content-type": "image/png", "content-length": icon.byteLength, "cache-control": "no-store" });
@@ -413,12 +509,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
413
509
  if (!authorized(request, port) || url.pathname !== "/ws" || typeof key !== "string") { socket.destroy(); return; }
414
510
  const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
415
511
  socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
416
- const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, backpressured: false, superseded: false };
512
+ const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, pendingDeliver: undefined, backpressured: false, superseded: false };
417
513
  socket.on("drain", () => {
418
514
  const pendingState = client.pendingState;
515
+ const pendingDeliver = client.pendingDeliver;
419
516
  client.pendingState = undefined;
517
+ client.pendingDeliver = undefined;
420
518
  client.backpressured = false;
421
- if (pendingState !== undefined) writeFrame(client, pendingState, true);
519
+ if (pendingState !== undefined) writeFrame(client, pendingState, true, pendingDeliver);
422
520
  });
423
521
  clients.add(client);
424
522
  socket.on("data", (chunk: unknown) => {
@@ -427,8 +525,10 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
427
525
  });
428
526
  socket.on("close", () => { disconnect(client); });
429
527
  socket.on("error", () => { disconnect(client); });
528
+ // A dropped tab only sends FIN, and an upgraded socket stays half-open until this side closes it.
529
+ socket.on("end", () => { socket.destroy(); });
430
530
  });
431
- server.once("listening", () => { void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt: Date.now() })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
531
+ server.once("listening", () => { startedAt = Date.now(); void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
432
532
  server.on("close", () => { closed = true; if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; } });
433
533
  return server;
434
534
  }