pi-oracle 0.6.2 → 0.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.4 - 2026-04-14
6
+
7
+ ### Changed
8
+ - bumped the local development and release toolchain to the latest published pi packages and current TypeScript/esbuild/Node type definitions so release verification now runs against the same pi generation shipping on this machine
9
+
10
+ ### Fixed
11
+ - `oracle_submit` archive creation now handles downstream `zstd` pipe failures as normal tool errors instead of crashing the host `pi` process with an unhandled `write EPIPE` on newer Node runtimes
12
+ - sanity coverage now exercises the broken-pipe archive path so early downstream compressor exits regress to a clean rejection instead of a process-level crash
13
+
14
+ ## 0.6.3 - 2026-04-13
15
+
16
+ ### Fixed
17
+ - workspace-root detection now prefers nearest project markers like `.pi/` and `AGENTS.md` over unrelated ancestor git roots, so oracle submissions from nested projects no longer widen to a home-directory repo and reject valid project-relative archive inputs like `AGENTS.md`
18
+ - sanity coverage now guards the nested-project-under-ancestor-git case so archive resolution stays anchored to the intended project root
19
+
5
20
  ## 0.6.2 - 2026-04-13
6
21
 
7
22
  ### Changed
@@ -6,7 +6,7 @@
6
6
  import { existsSync } from "node:fs";
7
7
  import { readFile } from "node:fs/promises";
8
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
9
- import { formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
9
+ import { formatOracleCancelOutcome, formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
10
10
  import { runOracleAuthBootstrap } from "./auth.js";
11
11
  import {
12
12
  cancelOracleJob,
@@ -14,6 +14,7 @@ import {
14
14
  isOpenOracleJob,
15
15
  isTerminalOracleJob,
16
16
  listJobsForCwd,
17
+ ORACLE_STALE_HEARTBEAT_MS,
17
18
  markWakeupSettled,
18
19
  readJob,
19
20
  reconcileStaleOracleJobs,
@@ -44,6 +45,7 @@ async function summarizeJob(jobId: string, options?: { responsePreview?: boolean
44
45
  artifactsPath: `${getJobDir(job.id)}/artifacts`,
45
46
  responseAvailable,
46
47
  responsePreview,
48
+ heartbeatStaleMs: ORACLE_STALE_HEARTBEAT_MS,
47
49
  });
48
50
  }
49
51
 
@@ -153,10 +155,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
153
155
  await promoteQueuedJobs({ workerPath, source: "oracle_cancel_command" });
154
156
  }
155
157
  refreshOracleStatus(ctx);
156
- const message = cancelled.status === "cancelled" || cancelled.status === "failed"
157
- ? `Cancelled oracle job ${cancelled.id}`
158
- : `Oracle job ${cancelled.id} was already ${cancelled.status}`;
159
- ctx.ui.notify(message, "info");
158
+ ctx.ui.notify(formatOracleCancelOutcome(cancelled), "info");
160
159
  },
161
160
  });
162
161
 
@@ -83,6 +83,19 @@ export function hasPersistedOriginSession(
83
83
  return typeof job.originSessionFile === "string" && job.originSessionFile.length > 0 && job.sessionId === job.originSessionFile;
84
84
  }
85
85
 
86
+ function hasActiveCancelIntent(job: Pick<OracleJob, "status" | "cancelRequestedAt">): boolean {
87
+ return !isTerminalOracleJob(job) && typeof job.cancelRequestedAt === "string" && job.cancelRequestedAt.length > 0;
88
+ }
89
+
90
+ function markCancelRequested(job: OracleJob, reason: string, at: string): OracleJob {
91
+ if (hasActiveCancelIntent(job)) return job;
92
+ return {
93
+ ...job,
94
+ cancelRequestedAt: at,
95
+ cancelReason: reason,
96
+ };
97
+ }
98
+
86
99
  export function isWorkerProcessAlive(pid: number | undefined, startedAt?: string): boolean {
87
100
  return isTrackedProcessAlive(pid, startedAt);
88
101
  }
@@ -113,6 +126,8 @@ export interface OracleJob {
113
126
  submittedAt?: string;
114
127
  completedAt?: string;
115
128
  heartbeatAt?: string;
129
+ cancelRequestedAt?: string;
130
+ cancelReason?: string;
116
131
  cwd: string;
117
132
  projectId: string;
118
133
  sessionId: string;
@@ -595,6 +610,7 @@ export async function reconcileStaleOracleJobs(): Promise<OracleJob[]> {
595
610
  const currentStaleReason = getStaleOracleJobReason(current, now);
596
611
  if (!currentStaleReason) return;
597
612
 
613
+ const cancelRequested = hasActiveCancelIntent(current);
598
614
  terminated = await terminateWorkerPid(current.workerPid, current.workerStartedAt);
599
615
  transitioned = true;
600
616
  const suffix = current.workerPid
@@ -602,19 +618,31 @@ export async function reconcileStaleOracleJobs(): Promise<OracleJob[]> {
602
618
  ? ` Terminated stale worker PID ${current.workerPid}.`
603
619
  : ` Failed to terminate stale worker PID ${current.workerPid}.`
604
620
  : "";
605
- repairedJob = transitionOracleJobPhase(current, "failed", {
606
- at: recoveredAt,
607
- source: "oracle:reconcile",
608
- message: `Recovered stale job: ${currentStaleReason}.${suffix}`.trim(),
609
- clearNotificationClaim: true,
610
- patch: {
611
- heartbeatAt: recoveredAt,
612
- cleanupPending: terminated,
613
- error: current.error
614
- ? `${current.error}\nRecovered stale job: ${currentStaleReason}.${suffix}`.trim()
615
- : `Recovered stale job: ${currentStaleReason}.${suffix}`.trim(),
616
- },
617
- });
621
+ repairedJob = cancelRequested && terminated
622
+ ? transitionOracleJobPhase(current, "cancelled", {
623
+ at: recoveredAt,
624
+ source: "oracle:reconcile",
625
+ message: `Recovered requested cancellation: ${currentStaleReason}.${suffix}`.trim(),
626
+ clearNotificationClaim: true,
627
+ patch: {
628
+ heartbeatAt: recoveredAt,
629
+ cleanupPending: true,
630
+ error: current.cancelReason ?? current.error ?? "Cancelled by user",
631
+ },
632
+ })
633
+ : transitionOracleJobPhase(current, "failed", {
634
+ at: recoveredAt,
635
+ source: "oracle:reconcile",
636
+ message: `Recovered stale job: ${currentStaleReason}.${suffix}`.trim(),
637
+ clearNotificationClaim: true,
638
+ patch: {
639
+ heartbeatAt: recoveredAt,
640
+ cleanupPending: terminated,
641
+ error: current.error
642
+ ? `${current.error}\nRecovered stale job: ${currentStaleReason}.${suffix}`.trim()
643
+ : `Recovered stale job: ${currentStaleReason}.${suffix}`.trim(),
644
+ },
645
+ });
618
646
  await writeJobUnlocked(repairedJob);
619
647
  });
620
648
 
@@ -803,30 +831,48 @@ export async function cancelOracleJob(id: string, reason = "Cancelled by user"):
803
831
  }));
804
832
  }
805
833
 
806
- const terminated = await terminateWorkerPid(current.workerPid, current.workerStartedAt);
834
+ let cancelTarget: Pick<OracleJob, "workerPid" | "workerStartedAt"> | undefined;
835
+ await withJobLock(id, { processPid: process.pid, action: "markCancelRequested", jobId: id }, async () => {
836
+ const latest = readJob(id);
837
+ if (!latest || isTerminalOracleJob(latest) || latest.status === "queued") return;
838
+ cancelTarget = { workerPid: latest.workerPid, workerStartedAt: latest.workerStartedAt };
839
+ const next = markCancelRequested(latest, reason, now);
840
+ if (next !== latest) {
841
+ await writeJobUnlocked(next);
842
+ }
843
+ });
844
+
845
+ const terminated = await terminateWorkerPid(cancelTarget?.workerPid, cancelTarget?.workerStartedAt);
807
846
  let transitioned = false;
808
- const cancelled = await updateJob(id, (job) => {
809
- if (isTerminalOracleJob(job)) return job;
847
+ let cancelled: OracleJob | undefined;
848
+ await withJobLock(id, { processPid: process.pid, action: "finalizeCancelOracleJob", jobId: id }, async () => {
849
+ const latest = readJob(id);
850
+ if (!latest) throw new Error(`Oracle job not found: ${id}`);
851
+ if (isTerminalOracleJob(latest)) {
852
+ cancelled = latest;
853
+ return;
854
+ }
810
855
  transitioned = true;
811
- return transitionOracleJobPhase(job, terminated ? "cancelled" : "failed", {
856
+ cancelled = transitionOracleJobPhase(latest, terminated ? "cancelled" : "failed", {
812
857
  at: now,
813
858
  source: "oracle:cancel",
814
859
  message: terminated
815
- ? `Job cancelled: ${reason}`
816
- : `Job cancellation failed because worker PID ${job.workerPid ?? "unknown"} did not exit.`,
860
+ ? `Job cancelled: ${latest.cancelReason ?? reason}`
861
+ : `Job cancellation failed because worker PID ${latest.workerPid ?? "unknown"} did not exit.`,
817
862
  clearNotificationClaim: true,
818
863
  patch: {
819
864
  heartbeatAt: now,
820
865
  cleanupPending: terminated,
821
- error: terminated ? reason : `${reason}; worker PID ${job.workerPid ?? "unknown"} did not exit`,
866
+ error: terminated ? latest.cancelReason ?? reason : `${latest.cancelReason ?? reason}; worker PID ${latest.workerPid ?? "unknown"} did not exit`,
822
867
  },
823
868
  });
869
+ await writeJobUnlocked(cancelled);
824
870
  });
825
- if (!transitioned) return cancelled;
871
+ if (!cancelled || !transitioned) return cancelled ?? readJob(id)!;
826
872
 
827
873
  if (!terminated) {
828
874
  const cleanupWarnings = [
829
- `Oracle runtime cleanup is blocked because worker PID ${current.workerPid ?? "unknown"} could not be terminated safely.`,
875
+ `Oracle runtime cleanup is blocked because worker PID ${cancelled.workerPid ?? cancelTarget?.workerPid ?? "unknown"} could not be terminated safely.`,
830
876
  ];
831
877
  return updateJob(id, (job) => applyOracleJobCleanupWarnings(job, cleanupWarnings, {
832
878
  at: now,
@@ -23,6 +23,8 @@ const PROFILE_CLONE_TIMEOUT_MS = 120_000;
23
23
  const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
24
24
  const WORKSPACE_ROOT_MARKERS = [
25
25
  ".pi/extensions/oracle.json",
26
+ ".pi",
27
+ "AGENTS.md",
26
28
  ] as const;
27
29
  const REQUIRED_ORACLE_DEPENDENCIES = [
28
30
  { name: "agent-browser", command: AGENT_BROWSER_BIN },
@@ -75,8 +77,8 @@ function resolveWorkspaceRoot(realCwd: string): string {
75
77
  let current = realCwd;
76
78
  let nearestMarkerRoot: string | undefined;
77
79
  while (true) {
78
- if (existsSync(join(current, ".git"))) return current;
79
80
  if (!nearestMarkerRoot && hasWorkspaceRootMarker(current)) nearestMarkerRoot = current;
81
+ if (existsSync(join(current, ".git"))) return nearestMarkerRoot ?? current;
80
82
  const parent = dirname(current);
81
83
  if (parent === current) return nearestMarkerRoot ?? realCwd;
82
84
  current = parent;
@@ -10,7 +10,7 @@ import { basename, join, posix } from "node:path";
10
10
  import { runOracleAuthBootstrap } from "./auth.js";
11
11
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
12
  import { Type } from "@sinclair/typebox";
13
- import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
13
+ import { formatOracleCancelOutcome, formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
14
14
  import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
15
15
  import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
16
16
  import {
@@ -31,6 +31,7 @@ import {
31
31
  isTerminalOracleJob,
32
32
  listOracleJobDirs,
33
33
  markWakeupSettled,
34
+ ORACLE_STALE_HEARTBEAT_MS,
34
35
  readJob,
35
36
  pruneTerminalOracleJobs,
36
37
  reconcileStaleOracleJobs,
@@ -89,6 +90,7 @@ const MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME = 1;
89
90
  const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = MAX_ARCHIVE_BYTES;
90
91
  const ARCHIVE_COMMAND_TIMEOUT_MS = 120_000;
91
92
  const ARCHIVE_COMMAND_KILL_GRACE_MS = 2_000;
93
+ const ARCHIVE_PIPE_FAILURE_ERROR_CODES = new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
92
94
 
93
95
  const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
94
96
  ".git",
@@ -159,6 +161,12 @@ function appendArchiveEntries(target: string[], source: Iterable<string>): void
159
161
  for (const entry of source) target.push(entry);
160
162
  }
161
163
 
164
+ function getErrorCode(error: unknown): string | undefined {
165
+ return error && typeof error === "object" && "code" in error && typeof error.code === "string"
166
+ ? error.code
167
+ : undefined;
168
+ }
169
+
162
170
  function mergeArchiveEntryGroups(groups: Iterable<Iterable<string>>): string[] {
163
171
  const merged: string[] = [];
164
172
  for (const group of groups) appendArchiveEntries(merged, group);
@@ -415,6 +423,18 @@ async function writeArchiveFile(
415
423
  else rejectPromise(new Error(stderr || `archive command failed (tar=${tarCode}, zstd=${zstdCode})`));
416
424
  };
417
425
 
426
+ const handlePipeError = (error: unknown) => {
427
+ const normalized = error instanceof Error ? error : new Error(String(error));
428
+ if (ARCHIVE_PIPE_FAILURE_ERROR_CODES.has(getErrorCode(normalized) ?? "")) {
429
+ stderr = `${stderr}${stderr ? "\n" : ""}${normalized.message}`;
430
+ tar.stdout.unpipe(zstd.stdin);
431
+ terminateChildren();
432
+ finish();
433
+ return;
434
+ }
435
+ finish(normalized);
436
+ };
437
+
418
438
  const commandTimeoutMs = options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS;
419
439
  if (commandTimeoutMs > 0) {
420
440
  timeout = setTimeout(() => {
@@ -433,6 +453,8 @@ async function writeArchiveFile(
433
453
  });
434
454
  tar.on("error", (error) => finish(error instanceof Error ? error : new Error(String(error))));
435
455
  zstd.on("error", (error) => finish(error instanceof Error ? error : new Error(String(error))));
456
+ tar.stdout.on("error", handlePipeError);
457
+ zstd.stdin.on("error", handlePipeError);
436
458
  tar.on("close", (code) => {
437
459
  tarCode = code;
438
460
  finish();
@@ -1378,6 +1400,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1378
1400
  artifactsPath: `${getJobDir(current.id)}/artifacts`,
1379
1401
  responsePreview,
1380
1402
  responseAvailable,
1403
+ heartbeatStaleMs: ORACLE_STALE_HEARTBEAT_MS,
1381
1404
  }),
1382
1405
  },
1383
1406
  ],
@@ -1419,7 +1442,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1419
1442
  }
1420
1443
  if (ctx.hasUI) refreshOracleStatus(ctx);
1421
1444
  return {
1422
- content: [{ type: "text", text: cancelled.status === "cancelled" || cancelled.status === "failed" ? `Cancelled oracle job ${cancelled.id}.` : `Oracle job ${cancelled.id} was already ${cancelled.status}.` }],
1445
+ content: [{ type: "text", text: formatOracleCancelOutcome(cancelled) }],
1423
1446
  details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
1424
1447
  };
1425
1448
  } catch (error) {
@@ -34,6 +34,8 @@ export interface OracleLifecycleTrackedJobLike {
34
34
  submittedAt?: string;
35
35
  completedAt?: string;
36
36
  heartbeatAt?: string;
37
+ cancelRequestedAt?: string;
38
+ cancelReason?: string;
37
39
  lifecycleEvents?: OracleJobLifecycleEvent[];
38
40
  cleanupPending?: boolean;
39
41
  cleanupWarnings?: string[];
@@ -167,7 +167,11 @@ export function transitionOracleJobPhase(job, phase, options = {}) {
167
167
  ? { submittedAt: patch.submittedAt ?? job.submittedAt ?? at }
168
168
  : {}),
169
169
  ...(TERMINAL_ORACLE_JOB_STATUSES.includes(status)
170
- ? { completedAt: patch.completedAt ?? job.completedAt ?? at }
170
+ ? {
171
+ completedAt: patch.completedAt ?? job.completedAt ?? at,
172
+ cancelRequestedAt: undefined,
173
+ cancelReason: undefined,
174
+ }
171
175
  : { completedAt: patch.completedAt ?? job.completedAt }),
172
176
  ...(options.clearNotificationClaim
173
177
  ? { notifyClaimedAt: undefined, notifyClaimedBy: undefined }
@@ -8,6 +8,7 @@ export interface OracleJobSummaryLike {
8
8
  queuedAt?: string;
9
9
  submittedAt?: string;
10
10
  completedAt?: string;
11
+ heartbeatAt?: string;
11
12
  projectId: string;
12
13
  sessionId: string;
13
14
  followUpToJobId?: string;
@@ -35,6 +36,8 @@ export interface OracleJobSummaryOptions {
35
36
  responseAvailable?: boolean;
36
37
  includeLatestEvent?: boolean;
37
38
  includeWorkerLogPath?: boolean;
39
+ nowMs?: number;
40
+ heartbeatStaleMs?: number;
38
41
  }
39
42
 
40
43
  export interface OracleSubmitResponseOptions {
@@ -51,6 +54,7 @@ export interface OracleStatusCounts {
51
54
 
52
55
  export declare function formatBytes(bytes: number): string;
53
56
  export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
57
+ export declare function formatOracleCancelOutcome(job: { id: string; status: string }): string;
54
58
  export declare function formatOracleJobSummary(job: OracleJobSummaryLike, options?: OracleJobSummaryOptions): string;
55
59
  export declare function buildOracleWakeupNotificationContent(
56
60
  job: OracleJobSummaryLike,
@@ -46,6 +46,69 @@ function formatAutoPrunedArchiveMessage(autoPrunedPrefixes) {
46
46
  return `Archive auto-pruned generic generated-output-name dirs to fit size limit: ${autoPrunedPrefixes.map((entry) => `${entry.relativePath}/ (${formatBytes(entry.bytes)})`).join(", ")}`;
47
47
  }
48
48
 
49
+ const ACTIVE_SUMMARY_STATUSES = new Set(["preparing", "submitted", "waiting"]);
50
+ const DEFAULT_ORACLE_HEARTBEAT_STALE_MS = 3 * 60 * 1000;
51
+
52
+ /**
53
+ * @param {string | undefined} value
54
+ * @returns {number | undefined}
55
+ */
56
+ function parseTimestamp(value) {
57
+ if (!value) return undefined;
58
+ const ms = Date.parse(value);
59
+ return Number.isNaN(ms) ? undefined : ms;
60
+ }
61
+
62
+ /**
63
+ * @param {number} elapsedMs
64
+ * @returns {string}
65
+ */
66
+ function formatElapsed(elapsedMs) {
67
+ const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
68
+ if (totalSeconds < 60) return `${totalSeconds}s`;
69
+ const totalMinutes = Math.floor(totalSeconds / 60);
70
+ const seconds = totalSeconds % 60;
71
+ if (totalMinutes < 60) return seconds === 0 ? `${totalMinutes}m` : `${totalMinutes}m ${String(seconds).padStart(2, "0")}s`;
72
+ const hours = Math.floor(totalMinutes / 60);
73
+ const minutes = totalMinutes % 60;
74
+ return minutes === 0 ? `${hours}h` : `${hours}h ${String(minutes).padStart(2, "0")}m`;
75
+ }
76
+
77
+ /**
78
+ * @param {OracleJobSummaryLike} job
79
+ * @param {OracleJobSummaryOptions} [options]
80
+ * @returns {string | undefined}
81
+ */
82
+ function formatHeartbeatFreshness(job, options = {}) {
83
+ if (!ACTIVE_SUMMARY_STATUSES.has(job.status)) return undefined;
84
+ const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
85
+ const staleMs = Number.isFinite(options.heartbeatStaleMs) ? options.heartbeatStaleMs : DEFAULT_ORACLE_HEARTBEAT_STALE_MS;
86
+ const heartbeatMs = parseTimestamp(job.heartbeatAt);
87
+ if (heartbeatMs !== undefined) {
88
+ const elapsedMs = Math.max(0, nowMs - heartbeatMs);
89
+ return `heartbeat: ${elapsedMs > staleMs ? "likely stale" : "fresh"} (${formatElapsed(elapsedMs)} ago)`;
90
+ }
91
+
92
+ const submittedMs = parseTimestamp(job.submittedAt);
93
+ const createdMs = parseTimestamp(job.createdAt);
94
+ const baselineMs = submittedMs ?? createdMs;
95
+ if (baselineMs === undefined) return "heartbeat: unavailable";
96
+
97
+ const elapsedMs = Math.max(0, nowMs - baselineMs);
98
+ const freshness = elapsedMs > staleMs ? "waiting for first worker update; likely stale" : "waiting for first worker update";
99
+ return `heartbeat: ${freshness} (${formatElapsed(elapsedMs)} ${submittedMs !== undefined ? "since submit" : "since create"})`;
100
+ }
101
+
102
+ /**
103
+ * @param {{ id: string; status: string }} job
104
+ * @returns {string}
105
+ */
106
+ export function formatOracleCancelOutcome(job) {
107
+ if (job.status === "cancelled") return `Cancelled oracle job ${job.id}.`;
108
+ if (job.status === "failed") return `Oracle job ${job.id} failed during cancellation.`;
109
+ return `Oracle job ${job.id} finished as ${job.status} before cancellation completed.`;
110
+ }
111
+
49
112
  /**
50
113
  * @param {OracleJobSummaryLike} job
51
114
  * @param {OracleJobSummaryOptions} [options]
@@ -78,6 +141,7 @@ export function formatOracleJobSummary(job, options = {}) {
78
141
  options.queuePosition ? `queue-position: ${options.queuePosition.position} of ${options.queuePosition.depth} global` : undefined,
79
142
  `project: ${job.projectId}`,
80
143
  `session: ${job.sessionId}`,
144
+ formatHeartbeatFreshness(job, options),
81
145
  job.completedAt ? `completed: ${job.completedAt}` : undefined,
82
146
  job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
83
147
  job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -62,13 +62,13 @@
62
62
  "basic-ftp": "^5.2.2"
63
63
  },
64
64
  "devDependencies": {
65
- "@mariozechner/pi-ai": "^0.65.2",
66
- "@mariozechner/pi-coding-agent": "^0.65.2",
65
+ "@mariozechner/pi-ai": "^0.67.2",
66
+ "@mariozechner/pi-coding-agent": "^0.67.2",
67
67
  "@sinclair/typebox": "^0.34.49",
68
- "@types/node": "^24.6.0",
69
- "esbuild": "^0.27.0",
70
- "tsx": "^4.20.6",
71
- "typescript": "^5.9.3"
68
+ "@types/node": "^25.6.0",
69
+ "esbuild": "^0.28.0",
70
+ "tsx": "^4.21.0",
71
+ "typescript": "^6.0.2"
72
72
  },
73
73
  "engines": {
74
74
  "node": ">=22"