pi-project-switcher 0.8.1 → 0.9.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/README.md +1 -1
- package/index.ts +266 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A [pi coding agent](https://github.com/earendil-works/pi) extension to switch be
|
|
|
14
14
|
- persists across reloads (session entry)
|
|
15
15
|
- sets the session display name
|
|
16
16
|
- injects the project path into every agent turn's system prompt, so file operations default to the active project
|
|
17
|
-
- **via the Telegram bridge**: the switch is confirmed in the chat — a short reply with the project, working directory, and session identity, plus buttons (project list, and switch back to the previous project). The confirmation is sent from the new session runtime, so it also works when the switch restores a stored session. No-switch outcomes (already active, cancelled, unknown) are answered in the chat too.
|
|
17
|
+
- **via the Telegram bridge**: the switch is confirmed in the chat — a short reply with the project, working directory, and session identity, plus buttons (project list, and switch back to the previous project). The confirmation is sent from the new session runtime, so it also works when the switch restores a stored session. No-switch outcomes (already active, cancelled, unknown) are answered in the chat too. When the switch released the Telegram transport (see below), the confirmation is **queued until the transport has verifiably re-armed** (fresh same-pid lock + active polling in `~/.pi/agent/tmp/telegram/state.json`, polled for up to 10 s) — a reply produced while the transport is down would be silently dropped. If the re-arm is not confirmed in time, the switcher instead sends a plain warning directly through the Telegram Bot API (token and chat id from `~/.pi/agent/telegram.json`): the switch happened, but the Telegram extension did not reconnect.
|
|
18
18
|
- **Telegram transport re-arm**: a session-replacing switch from the Telegram bridge loses the bridge's transport: pi-telegram stands down on session shutdown, and its reconnect cannot take over the lock across the restored session's different cwd (same-pid locks never go stale; same-process takeover requires matching cwd). The switcher therefore transitions the transport across the switch itself: before the session switch it executes `/telegram-disconnect` (which releases pi-telegram's lock), and after the switch it re-executes `/telegram-connect` from the new session (~3 s delay) — but **only** when the session being left provably owned the connected transport: pi-telegram's lock (`~/.pi/agent/tmp/telegram/owners.json`) must name this process with a fresh heartbeat and a cwd matching the old session. A cancelled switch reconnects immediately. Native switches and switches from sessions that didn't own the bot never touch the transport. The lock file is only read, never modified.
|
|
19
19
|
- **if the project doesn't exist yet**, offers to create the folder and switch to it (confirmation dialog on dialog-capable surfaces; use `/project <name>!` to skip the dialog — e.g. on headless/RPC surfaces). Unsafe names (path segments, `..`, hidden, absolute) are never created.
|
|
20
20
|
- **Session restore** — a machine-local map (`~/.pi/agent/project-switcher-sessions.json`) remembers the most recent session per project. Switching projects returns you to that project's last session; if none exists (or the file is gone), the switch happens in the current session.
|
package/index.ts
CHANGED
|
@@ -29,6 +29,10 @@ const HOME = homedir();
|
|
|
29
29
|
const ENTRY_TYPE = "project-switcher-state";
|
|
30
30
|
const SESSIONS_DIR = join(HOME, ".pi", "agent", "sessions");
|
|
31
31
|
const SESSION_MAP_PATH = join(HOME, ".pi", "agent", "project-switcher-sessions.json");
|
|
32
|
+
const TELEGRAM_TMP_DIR = join(HOME, ".pi", "agent", "tmp", "telegram");
|
|
33
|
+
const TELEGRAM_STATE_PATH = join(TELEGRAM_TMP_DIR, "state.json");
|
|
34
|
+
const TELEGRAM_OWNERS_PATH = join(TELEGRAM_TMP_DIR, "owners.json");
|
|
35
|
+
const TELEGRAM_CONFIG_PATH = join(HOME, ".pi", "agent", "telegram.json");
|
|
32
36
|
|
|
33
37
|
interface Config {
|
|
34
38
|
baseDir: string;
|
|
@@ -344,7 +348,15 @@ function buildTelegramNoChangePrompt(text: string): string {
|
|
|
344
348
|
* bridge's diagnosis guidance.
|
|
345
349
|
*/
|
|
346
350
|
function telegramOwnersPath(): string {
|
|
347
|
-
return
|
|
351
|
+
return telegramPaths.owners ?? TELEGRAM_OWNERS_PATH;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* pi-telegram's runtime state snapshot — written on a scheduler and on
|
|
356
|
+
* status changes; read-only for the switcher.
|
|
357
|
+
*/
|
|
358
|
+
function telegramStatePath(): string {
|
|
359
|
+
return telegramPaths.state ?? TELEGRAM_STATE_PATH;
|
|
348
360
|
}
|
|
349
361
|
|
|
350
362
|
/**
|
|
@@ -363,6 +375,39 @@ const TELEGRAM_OWNERSHIP_FRESH_MS = 10_000;
|
|
|
363
375
|
*/
|
|
364
376
|
const TELEGRAM_REARM_DELAY_MS = 3_000;
|
|
365
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Poll interval for the re-arm verification: how often the state snapshot
|
|
380
|
+
* and lock heartbeat are re-read while waiting for the transport to come
|
|
381
|
+
* back after the reconnect dispatch.
|
|
382
|
+
*/
|
|
383
|
+
const TELEGRAM_REARM_POLL_MS = 500;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Bound for the re-arm verification, measured from the moment the
|
|
387
|
+
* /telegram-connect dispatch completes. When the transport has not
|
|
388
|
+
* verifiably re-armed within this window, the queued confirmation falls
|
|
389
|
+
* back to the direct Bot-API warning (its follow-up reply would be
|
|
390
|
+
* undeliverable anyway while the transport is down).
|
|
391
|
+
*/
|
|
392
|
+
const TELEGRAM_REARM_BOUND_MS = 10_000;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Test seam: paths for the re-arm verification reads. Tests point these
|
|
396
|
+
* at fixture files; production code always uses the real paths.
|
|
397
|
+
*/
|
|
398
|
+
export const telegramPaths: { state?: string; owners?: string } = {};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Test seam: Bot-API warning sender override. When set, the fallback uses
|
|
402
|
+
* this instead of reading telegram.json and calling the Telegram HTTPS
|
|
403
|
+
* API. Signature: (text) => Promise<void> (may throw; callers guard).
|
|
404
|
+
*/
|
|
405
|
+
export let botApiWarningSender: ((text: string) => Promise<void>) | null = null;
|
|
406
|
+
|
|
407
|
+
export function setBotApiWarningSender(sender: ((text: string) => Promise<void>) | null): void {
|
|
408
|
+
botApiWarningSender = sender;
|
|
409
|
+
}
|
|
410
|
+
|
|
366
411
|
/**
|
|
367
412
|
* Release the Telegram transport from the OLD (owning) runtime, before
|
|
368
413
|
* the session is replaced. pi's prompt() executes extension commands
|
|
@@ -423,19 +468,173 @@ function probeTelegramTransportOwnership(oldCwd: string): boolean {
|
|
|
423
468
|
}
|
|
424
469
|
|
|
425
470
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
471
|
+
* Read-only re-arm verification: has the transport verifiably come back in
|
|
472
|
+
* THIS process after the reconnect dispatch? True iff (a) the ownership
|
|
473
|
+
* lock has a fresh entry for the current process (the connect handler
|
|
474
|
+
* acquires the lock synchronously) and (b) pi-telegram's state snapshot
|
|
475
|
+
* shows polling as active (or is not yet readable — the snapshot is
|
|
476
|
+
* written on a scheduler, so the fresh lock heartbeat is the primary
|
|
477
|
+
* signal and the snapshot must not contradict it with polling stopped).
|
|
478
|
+
*/
|
|
479
|
+
function probeTelegramRearmConfirmed(): boolean {
|
|
480
|
+
// (a) fresh same-pid lock entry (cwd irrelevant: the new runtime has the
|
|
481
|
+
// new session's cwd by design — the re-arm is exactly the ownership
|
|
482
|
+
// handoff across that cwd change).
|
|
483
|
+
let ownsFreshLock = false;
|
|
484
|
+
try {
|
|
485
|
+
const raw = JSON.parse(readFileSync(telegramOwnersPath(), "utf8"));
|
|
486
|
+
if (raw && typeof raw === "object") {
|
|
487
|
+
for (const entry of Object.values(raw) as any[]) {
|
|
488
|
+
if (
|
|
489
|
+
entry &&
|
|
490
|
+
typeof entry === "object" &&
|
|
491
|
+
entry.pid === process.pid &&
|
|
492
|
+
typeof entry.heartbeatMs === "number" &&
|
|
493
|
+
Date.now() - entry.heartbeatMs <= TELEGRAM_OWNERSHIP_FRESH_MS
|
|
494
|
+
) {
|
|
495
|
+
ownsFreshLock = true;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
return false; // missing/malformed lock: not re-armed
|
|
502
|
+
}
|
|
503
|
+
if (!ownsFreshLock) return false;
|
|
504
|
+
|
|
505
|
+
// (b) state snapshot: polling must not be stopped. A missing/unreadable
|
|
506
|
+
// snapshot does not block confirmation (the lock is authoritative for
|
|
507
|
+
// ownership; polling starts with the connect).
|
|
508
|
+
try {
|
|
509
|
+
const raw = JSON.parse(readFileSync(telegramStatePath(), "utf8"));
|
|
510
|
+
const pollingActive = raw?.runtime?.pollingActive;
|
|
511
|
+
if (typeof pollingActive === "boolean" && !pollingActive) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
// snapshot absent or stale: rely on the lock heartbeat alone
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Bounded wait for the verified re-arm. Polls the lock + state snapshot at
|
|
522
|
+
* a short interval for up to TELEGRAM_REARM_BOUND_MS. Never throws.
|
|
523
|
+
*/
|
|
524
|
+
async function waitForTelegramRearm(): Promise<{ confirmed: boolean; reason?: string }> {
|
|
525
|
+
const deadline = Date.now() + TELEGRAM_REARM_BOUND_MS;
|
|
526
|
+
for (;;) {
|
|
527
|
+
if (probeTelegramRearmConfirmed()) {
|
|
528
|
+
return { confirmed: true };
|
|
529
|
+
}
|
|
530
|
+
if (Date.now() >= deadline) {
|
|
531
|
+
return {
|
|
532
|
+
confirmed: false,
|
|
533
|
+
reason: "telegram transport did not re-arm within 10s of the reconnect dispatch",
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
await new Promise((resolve) => setTimeout(resolve, TELEGRAM_REARM_POLL_MS));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Direct Telegram Bot-API warning fallback. Reads the bot token and the
|
|
542
|
+
* allowed user id from ~/.pi/agent/telegram.json and sends one plain
|
|
543
|
+
* message via the public HTTPS API — usable precisely when the extension
|
|
544
|
+
* transport is dead. Never throws; failures are returned as a reason so
|
|
545
|
+
* the caller can journal them. `project` names the switched-to project.
|
|
546
|
+
*/
|
|
547
|
+
async function sendBotApiRearmWarning(project: string): Promise<string | null> {
|
|
548
|
+
if (botApiWarningSender) {
|
|
549
|
+
try {
|
|
550
|
+
await botApiWarningSender(`⚠️ Switched to ${project}, but reconnecting the pi Telegram extension did not work — Telegram commands may not reach the agent until it reconnects (/telegram-connect).`);
|
|
551
|
+
return null;
|
|
552
|
+
} catch (err: any) {
|
|
553
|
+
return `Bot-API warning send failed: ${err?.message ?? err}`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Read token + chat id from the bridge configuration.
|
|
558
|
+
let token = "";
|
|
559
|
+
let chatId: number | undefined;
|
|
560
|
+
try {
|
|
561
|
+
const raw = JSON.parse(readFileSync(TELEGRAM_CONFIG_PATH, "utf8"));
|
|
562
|
+
const profile = raw?.profiles?.default ?? raw;
|
|
563
|
+
token = typeof profile?.botToken === "string" ? profile.botToken : "";
|
|
564
|
+
chatId = typeof profile?.allowedUserId === "number" ? profile.allowedUserId : undefined;
|
|
565
|
+
} catch {
|
|
566
|
+
return "Bot-API warning skipped: ~/.pi/agent/telegram.json unreadable";
|
|
567
|
+
}
|
|
568
|
+
if (!token || chatId === undefined) {
|
|
569
|
+
return "Bot-API warning skipped: bot token or allowed user id missing";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const text =
|
|
573
|
+
`⚠️ Switched to ${project}, but reconnecting the pi Telegram extension did not work ` +
|
|
574
|
+
`— Telegram commands may not reach the agent until it reconnects (/telegram-connect).`;
|
|
575
|
+
try {
|
|
576
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers: { "Content-Type": "application/json" },
|
|
579
|
+
body: JSON.stringify({ chat_id: chatId, text }),
|
|
580
|
+
});
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
return `Bot-API warning failed: HTTP ${response.status}`;
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
} catch (err: any) {
|
|
586
|
+
return `Bot-API warning send failed: ${err?.message ?? err}`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
interface QueuedTelegramFollowUp {
|
|
591
|
+
/** Prompt for the confirmation follow-up turn (built at queue time). */
|
|
592
|
+
prompt: string;
|
|
593
|
+
/** Project name, for the Bot-API fallback warning. */
|
|
594
|
+
project: string;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Pending combined re-arm callback (single slot): scheduling a new one
|
|
599
|
+
* supersedes a not-yet-fired previous one. The callback only touches the
|
|
600
|
+
* withSession context it was scheduled with, so it can never act on a
|
|
601
|
+
* stale runtime.
|
|
429
602
|
*/
|
|
430
603
|
let pendingRearmTimer: ReturnType<typeof setTimeout> | null = null;
|
|
431
604
|
|
|
432
|
-
function
|
|
605
|
+
function clearPendingRearm(): void {
|
|
433
606
|
if (pendingRearmTimer) {
|
|
434
607
|
clearTimeout(pendingRearmTimer);
|
|
435
608
|
pendingRearmTimer = null;
|
|
436
609
|
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Schedule the transport re-arm from the fresh runtime: after the safety
|
|
614
|
+
* delay, re-dispatch /telegram-connect, wait until the re-arm is VERIFIED
|
|
615
|
+
* (bounded), then dispatch the queued confirmation follow-up. When the
|
|
616
|
+
* re-arm does not confirm within the bound (or the connect dispatch
|
|
617
|
+
* fails), the confirmation turn is NOT dispatched (its reply would be
|
|
618
|
+
* undeliverable); instead a plain warning goes out directly through the
|
|
619
|
+
* Telegram Bot API and the failure is journaled locally. Every step is
|
|
620
|
+
* guarded: an error inside this timer callback must never kill the daemon.
|
|
621
|
+
*/
|
|
622
|
+
function scheduleTelegramRearm(
|
|
623
|
+
newCtx: any,
|
|
624
|
+
source: string,
|
|
625
|
+
queuedFollowUp: QueuedTelegramFollowUp | null,
|
|
626
|
+
): void {
|
|
627
|
+
clearPendingRearm();
|
|
437
628
|
const timer = setTimeout(async () => {
|
|
438
629
|
pendingRearmTimer = null;
|
|
630
|
+
const journal = (message: string) => {
|
|
631
|
+
try {
|
|
632
|
+
newCtx.ui.notify(message, "warning");
|
|
633
|
+
} catch {
|
|
634
|
+
// The fresh context can be gone (e.g. another switch followed):
|
|
635
|
+
// never let an error escape into an uncaught timer callback.
|
|
636
|
+
}
|
|
637
|
+
};
|
|
439
638
|
try {
|
|
440
639
|
// Command re-dispatch from the FRESH runtime: executes pi-telegram's
|
|
441
640
|
// connect handler (no agent turn). The lock was released by the
|
|
@@ -446,16 +645,36 @@ function scheduleTelegramRearm(newCtx: any, source: string): void {
|
|
|
446
645
|
expandPromptTemplates: true,
|
|
447
646
|
});
|
|
448
647
|
} catch (err: any) {
|
|
648
|
+
journal(`Telegram re-arm after ${source} switch failed: ${err?.message ?? err}`);
|
|
649
|
+
if (queuedFollowUp) {
|
|
650
|
+
const warnError = await sendBotApiRearmWarning(queuedFollowUp.project);
|
|
651
|
+
if (warnError) journal(warnError);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!queuedFollowUp) return;
|
|
657
|
+
|
|
658
|
+
// Wait for the verified re-arm before dispatching the confirmation:
|
|
659
|
+
// a follow-up reply finishing while the transport is still down is
|
|
660
|
+
// dropped silently by pi-telegram (observed 2026-09-22).
|
|
661
|
+
const rearm = await waitForTelegramRearm();
|
|
662
|
+
if (rearm.confirmed) {
|
|
449
663
|
try {
|
|
450
|
-
newCtx.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// Even the fresh context can be gone (e.g. another switch followed):
|
|
456
|
-
// never let an error escape into an uncaught timer callback.
|
|
664
|
+
await newCtx.sendUserMessage(queuedFollowUp.prompt, {
|
|
665
|
+
deliverAs: "followUp",
|
|
666
|
+
});
|
|
667
|
+
} catch (err: any) {
|
|
668
|
+
journal(`Telegram confirmation after ${source} switch failed: ${err?.message ?? err}`);
|
|
457
669
|
}
|
|
670
|
+
return;
|
|
458
671
|
}
|
|
672
|
+
|
|
673
|
+
// Re-arm not verifiable in time: the follow-up reply would be
|
|
674
|
+
// undeliverable — warn directly through the Bot API instead.
|
|
675
|
+
journal(`Telegram re-arm verification after ${source} switch timed out: ${rearm.reason}`);
|
|
676
|
+
const warnError = await sendBotApiRearmWarning(queuedFollowUp.project);
|
|
677
|
+
if (warnError) journal(warnError);
|
|
459
678
|
}, TELEGRAM_REARM_DELAY_MS);
|
|
460
679
|
timer.unref?.();
|
|
461
680
|
pendingRearmTimer = timer;
|
|
@@ -712,6 +931,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
712
931
|
// with "stale ctx" errors and killed the whole flow).
|
|
713
932
|
const branch = getGitBranch(projectPath(name));
|
|
714
933
|
const branchStr = branch ? ` on branch \`${branch}\`` : "";
|
|
934
|
+
const confirmPrompt = buildTelegramSwitchConfirmationPrompt({
|
|
935
|
+
project: name,
|
|
936
|
+
path: projectPath(name),
|
|
937
|
+
branchStr,
|
|
938
|
+
sessionLine: `🗂 Session restored: ${basename(targetSession)}`,
|
|
939
|
+
previous,
|
|
940
|
+
});
|
|
715
941
|
// Ownership probe BEFORE the switch (the old session's cwd is only
|
|
716
942
|
// available here): when the session we are leaving is the live owner
|
|
717
943
|
// of the connected Telegram transport, the transport must be carried
|
|
@@ -744,29 +970,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
744
970
|
`Workdir: ${projectPath(name)}${branchStr ? ` ${branchStr}` : ""}`,
|
|
745
971
|
"info"
|
|
746
972
|
);
|
|
747
|
-
if (isTelegramOrigin) {
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
973
|
+
if (isTelegramOrigin && releasedTelegramTransport) {
|
|
974
|
+
// The pre-switch disconnect released the transport; the new
|
|
975
|
+
// runtime re-arms it after a short safety delay. The
|
|
976
|
+
// confirmation turn must NOT be dispatched until the re-arm is
|
|
977
|
+
// VERIFIED: a follow-up reply finishing while the transport is
|
|
978
|
+
// still down is dropped silently by pi-telegram (observed
|
|
979
|
+
// 2026-09-22). Queue it with the re-arm: connect → verified
|
|
980
|
+
// polling → confirmation from this fresh context; on timeout,
|
|
981
|
+
// a plain warning goes out directly via the Telegram Bot API.
|
|
982
|
+
scheduleTelegramRearm(newCtx, name, { prompt: confirmPrompt, project: name });
|
|
983
|
+
} else if (isTelegramOrigin) {
|
|
984
|
+
// No transport transition (probe failed): the transport was
|
|
985
|
+
// never released, so there is no dead window — the
|
|
986
|
+
// confirmation turn is safe immediately. The turn also settles
|
|
987
|
+
// pi-telegram's dispatch queue.
|
|
752
988
|
await newCtx.waitForIdle();
|
|
753
|
-
await newCtx.sendUserMessage(
|
|
754
|
-
buildTelegramSwitchConfirmationPrompt({
|
|
755
|
-
project: name,
|
|
756
|
-
path: projectPath(name),
|
|
757
|
-
branchStr,
|
|
758
|
-
sessionLine: `🗂 Session restored: ${basename(targetSession)}`,
|
|
759
|
-
previous,
|
|
760
|
-
}),
|
|
761
|
-
{ deliverAs: "followUp" }
|
|
762
|
-
);
|
|
989
|
+
await newCtx.sendUserMessage(confirmPrompt, { deliverAs: "followUp" });
|
|
763
990
|
}
|
|
764
991
|
if (releasedTelegramTransport) {
|
|
765
|
-
// Re-arm the Telegram transport from the fresh runtime
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
|
|
992
|
+
// Re-arm the Telegram transport from the fresh runtime (queued
|
|
993
|
+
// together with the confirmation above when both apply). The
|
|
994
|
+
// lock was released by the pre-switch disconnect, so this
|
|
995
|
+
// connect acquires unconditionally after the short safety
|
|
996
|
+
// delay. Only the withSession context is touched.
|
|
997
|
+
if (!isTelegramOrigin) {
|
|
998
|
+
scheduleTelegramRearm(newCtx, name, null);
|
|
999
|
+
}
|
|
770
1000
|
}
|
|
771
1001
|
},
|
|
772
1002
|
});
|
|
@@ -928,3 +1158,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
928
1158
|
},
|
|
929
1159
|
});
|
|
930
1160
|
}
|
|
1161
|
+
|
|
1162
|
+
// ── Test-only hooks (never imported in production) ────────────────────────
|
|
1163
|
+
export const __testProbeRearm = probeTelegramRearmConfirmed;
|
|
1164
|
+
export const __testClearRearm = clearPendingRearm;
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":
|
|
1
|
+
{"name":"pi-project-switcher","version":"0.9.0","description":"pi coding agent extension: switch between projects under a configurable base directory via /project","main":"index.ts","type":"module","scripts":{"test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit"},"keywords":["pi","pi-package","pi-extension","project","switcher","project-switching"],"author":"stefclawd","license":"MIT","repository":{"type":"git","url":"git+https://github.com/stefclawd/pi-project-switcher.git"},"bugs":{"url":"https://github.com/stefclawd/pi-project-switcher/issues"},"homepage":"https://github.com/stefclawd/pi-project-switcher#readme","files":["index.ts","README.md","LICENSE"],"engines":{"node":">=22.19.0"},"pi":{"extensions":["./index.ts"]},"devDependencies":{"@earendil-works/pi-coding-agent":"^0.85.1","@types/node":"^24.0.0","typescript":"^5.7.0","vitest":"^3.0.0"}}
|