pisesh 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.0] - 2026-08-22
6
+
7
+ ### Changed
8
+ - Make `/sesh` return the selected session to its extension and switch through pi's official `ctx.switchSession()` API instead of launching a nested pi process. Standalone `pisesh` still launches pi.
9
+ - Preserve `Enter` current-default and `o` session-recorded model and thinking behavior across native session switches.
10
+
11
+ ### Fixed
12
+ - Forward custom cwd overrides when supported by pi, warn when pi ignores them, and skip switching when the selected session is already active.
13
+ - Keep interrupted-tool-call repair after confirming pi does not synthesize missing results during session loading.
14
+
5
15
  ## [0.2.0] - 2026-08-01
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -73,6 +73,8 @@ pi install npm:pisesh
73
73
 
74
74
  This registers pisesh as a pi extension. Inside any pi session, type `/sesh`. The extension runs its bundled CLI, so a global npm installation is not required.
75
75
 
76
+ `/sesh` does not start a second pi process. The picker returns the selected session and options to the extension, which calls pi's official `ctx.switchSession()` API. Standalone `pisesh` keeps its shell behavior and starts `pi --session`. Custom cwd overrides require a pi version that supports `cwdOverride` on extension session switches; pisesh warns if pi ignores one.
77
+
76
78
  ### Install the standalone CLI
77
79
 
78
80
  ```bash
@@ -142,10 +144,10 @@ pisesh --help
142
144
  | Alt screen buffer | `\x1b[?1049h` / `\x1b[?1049l`, the same primitive `vim`, `less`, `htop`, and droid CLI use |
143
145
  | Input | Node's `readline.emitKeypressEvents` in raw mode |
144
146
  | Width calculation | UAX #11 East Asian Width ranges, compressed to ~10 inline range checks |
145
- | Pi extension | TypeScript factory using `@earendil-works/pi-coding-agent` extension API (`ui.custom`, `tui.stop`) |
147
+ | Pi extension | TypeScript factory using `ui.custom`, `tui.stop`, and `ctx.switchSession()` |
146
148
  | Storage | Two JSON files under `$PI_AGENT_DIR`: `favorites.json` and `pisesh-meta.json` |
147
149
  | Session discovery | Direct filesystem scan of `~/.pi/agent/sessions/<projectSlug>/*.jsonl`; first 96 KB parsed |
148
- | Process model | Slash command pauses pi's TUI, spawns pisesh with inherited stdio, restarts pi on exit |
150
+ | Process model | `/sesh` runs pisesh as a selector and switches the current runtime; standalone starts `pi` |
149
151
  | Resume settings | `Enter` uses current defaults; `o` preserves the model and thinking recorded in the session |
150
152
  | Custom paths | Honors `PI_AGENT_DIR` and `PI_SESSION_DIR`, including a flat custom session directory |
151
153
  | Title generation | Ephemeral `pi --print --no-session` call using the model and effort selected in pisesh |
@@ -198,7 +200,7 @@ Korean / Chinese / Japanese / fullwidth characters render **2 cells wide** in te
198
200
  - Windows: **Windows Terminal**, **WezTerm**, **Alacritty** ✅
199
201
  - macOS: **iTerm2**, **Terminal.app**, **WezTerm**, **Alacritty**, **Kitty** ✅
200
202
  - Linux: **GNOME Terminal**, **Konsole**, **xterm**, **Alacritty**, **Kitty** ✅
201
- - [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) on `$PATH` for the `Enter`-to-resume action
203
+ - [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) on `$PATH` when using standalone `pisesh`
202
204
 
203
205
  ## Contributing
204
206
 
package/bin/pisesh CHANGED
@@ -24,6 +24,12 @@ function piAgentEnv(baseEnv = process.env, agentDir = AGENT_DIR) {
24
24
  return { ...baseEnv, PI_CODING_AGENT_DIR: agentDir };
25
25
  }
26
26
  const PI_ENV = piAgentEnv();
27
+ // The /sesh extension gives the picker a private result pipe. Standalone
28
+ // pisesh has no such fd and keeps launching pi itself.
29
+ const SELECT_FD = (() => {
30
+ const fd = Number(process.env.PISESH_SELECT_FD);
31
+ return Number.isInteger(fd) && fd >= 3 ? fd : null;
32
+ })();
27
33
  const VERSION = (() => {
28
34
  for (const file of [
29
35
  path.resolve(__dirname, '../package.json'),
@@ -1477,6 +1483,38 @@ function loadResumeDefaults(settingsFile = SETTINGS_FILE) {
1477
1483
  } catch { return null; }
1478
1484
  }
1479
1485
 
1486
+ function loadSessionSettings(file) {
1487
+ try {
1488
+ const entries = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap(line => {
1489
+ try { return [JSON.parse(line)]; } catch { return []; }
1490
+ }).filter(entry => entry && entry.type !== 'session');
1491
+ const byId = new Map(entries.filter(entry => entry.id).map(entry => [entry.id, entry]));
1492
+ const branch = [];
1493
+ const visited = new Set();
1494
+ let entry = entries[entries.length - 1];
1495
+ while (entry && !visited.has(entry.id)) {
1496
+ branch.push(entry);
1497
+ visited.add(entry.id);
1498
+ entry = entry.parentId ? byId.get(entry.parentId) : undefined;
1499
+ }
1500
+ branch.reverse();
1501
+
1502
+ let model = '';
1503
+ let thinking = '';
1504
+ for (const item of branch) {
1505
+ if (item.type === 'thinking_level_change' && typeof item.thinkingLevel === 'string') {
1506
+ thinking = item.thinkingLevel;
1507
+ } else if (item.type === 'model_change' && typeof item.provider === 'string' && typeof item.modelId === 'string') {
1508
+ model = `${item.provider}/${item.modelId}`;
1509
+ } else if (item.type === 'message' && item.message?.role === 'assistant'
1510
+ && typeof item.message.provider === 'string' && typeof item.message.model === 'string') {
1511
+ model = `${item.message.provider}/${item.message.model}`;
1512
+ }
1513
+ }
1514
+ return { model, thinking };
1515
+ } catch { return null; }
1516
+ }
1517
+
1480
1518
  function buildResumeArgs(s, useCurrentDefaults = true, settingsFile = SETTINGS_FILE) {
1481
1519
  const args = ['--session', s.id, '--session-dir', path.dirname(s.file)];
1482
1520
  const defaults = useCurrentDefaults && loadResumeDefaults(settingsFile);
@@ -1485,27 +1523,70 @@ function buildResumeArgs(s, useCurrentDefaults = true, settingsFile = SETTINGS_F
1485
1523
  return args;
1486
1524
  }
1487
1525
 
1526
+ function buildSelection(s, useCurrentDefaults = true, repaired = 0, settingsFile = SETTINGS_FILE) {
1527
+ const defaults = loadResumeDefaults(settingsFile);
1528
+ const recorded = useCurrentDefaults ? null : loadSessionSettings(s.file);
1529
+ const settings = useCurrentDefaults ? defaults : {
1530
+ model: recorded?.model || defaults?.model || '',
1531
+ thinking: recorded?.thinking || defaults?.thinking || '',
1532
+ };
1533
+ return {
1534
+ version: 1,
1535
+ sessionPath: path.resolve(s.file),
1536
+ resumeMode: useCurrentDefaults ? 'defaults' : 'session',
1537
+ ...(s.cwdOverride ? { cwdOverride: s.cwdOverride } : {}),
1538
+ ...(settings?.model ? { model: settings.model } : {}),
1539
+ ...(settings?.thinking ? { thinking: settings.thinking } : {}),
1540
+ ...(repaired > 0 ? { repaired } : {}),
1541
+ };
1542
+ }
1543
+
1544
+ function leaveTui() {
1545
+ process.stdout.write(A.exitAlt + A.showC);
1546
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1547
+ process.stdin.pause();
1548
+ }
1549
+
1550
+ function returnSelection(selection) {
1551
+ try {
1552
+ fs.writeSync(SELECT_FD, `${JSON.stringify(selection)}\n`);
1553
+ } catch (error) {
1554
+ throw new Error(`could not return selection to pi: ${error.message}`);
1555
+ }
1556
+ }
1557
+
1488
1558
  function resumeSession(s, useCurrentDefaults = true) {
1489
1559
  if (!s) return;
1490
- // A resumed pi takes over this terminal, so stop background title work first
1491
- // and suppress any final render from its asynchronous close handler.
1560
+ // A resumed pi or the parent extension takes over this terminal, so stop
1561
+ // background title work and suppress its asynchronous final render.
1492
1562
  handingOff = true;
1493
1563
  cancelGeneration();
1494
- // Heal orphaned tool calls before handing off, so resume can't crash the
1495
- // spawned pi (see healOrphanedToolCalls).
1564
+
1565
+ // Selecting the already-active session must not rewrite its live transcript.
1566
+ if (SELECT_FD !== null && s.isCurrent) {
1567
+ leaveTui();
1568
+ try { returnSelection(buildSelection(s, useCurrentDefaults)); }
1569
+ catch (error) { console.error(`pisesh: ${error.message}`); process.exit(1); }
1570
+ process.exit(0);
1571
+ }
1572
+
1573
+ // Pi still preserves a valid unfinished toolCall without adding its missing
1574
+ // result, so repair it before either native switching or standalone resume.
1496
1575
  let repaired;
1497
1576
  try { repaired = healOrphanedToolCalls(s.file); }
1498
1577
  catch (error) {
1499
- process.stdout.write(A.exitAlt + A.showC);
1500
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1578
+ leaveTui();
1501
1579
  console.error(`pisesh: ${error.message}`);
1502
1580
  process.exit(1);
1503
1581
  }
1504
- // Leave alt screen + show cursor so the spawned pi takes over a clean
1505
- // main-buffer terminal (it will manage its own alt screen).
1506
- process.stdout.write(A.exitAlt + A.showC);
1507
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1508
- process.stdin.pause();
1582
+ leaveTui();
1583
+
1584
+ if (SELECT_FD !== null) {
1585
+ try { returnSelection(buildSelection(s, useCurrentDefaults, repaired)); }
1586
+ catch (error) { console.error(`pisesh: ${error.message}`); process.exit(1); }
1587
+ process.exit(0);
1588
+ }
1589
+
1509
1590
  if (repaired > 0) {
1510
1591
  console.log(`pisesh: repaired ${repaired} orphaned session entr${repaired === 1 ? 'y' : 'ies'}; backup: ${s.file}.bak-orphanheal`);
1511
1592
  }
@@ -1640,11 +1721,13 @@ function main() {
1640
1721
  if (require.main === module) main();
1641
1722
  else module.exports = {
1642
1723
  buildResumeArgs,
1724
+ buildSelection,
1643
1725
  buildTitlePrompt,
1644
1726
  cleanGeneratedTitle,
1645
1727
  cleanStaleFavorites,
1646
1728
  healOrphanedToolCalls,
1647
1729
  loadResumeDefaults,
1730
+ loadSessionSettings,
1648
1731
  parseModelEntries,
1649
1732
  parseModelList,
1650
1733
  piAgentEnv,
@@ -1,97 +1,269 @@
1
1
  /**
2
2
  * pisesh slash command
3
3
  *
4
- * Registers `/sesh` inside pi.
5
- *
6
- * Behavior:
7
- * 1. Pauses pi's TUI (releases the terminal)
8
- * 2. Spawns the external `pisesh` TUI (bookmark/resume picker)
9
- * 3. When pisesh exits — whether the user resumed a nested session and quit it,
10
- * or just pressed `q` — control returns to the original pi session and the
11
- * TUI is restored.
12
- *
13
- * Bundled CLI tool: ../bin/pisesh (single-file Node TUI, no deps)
14
- * Favorites file: ~/.pi/agent/favorites.json
4
+ * `/sesh` temporarily hands the terminal to the bundled picker. The picker
5
+ * returns a session path on a private fd; this extension then asks pi to switch
6
+ * its current runtime. Standalone `pisesh` still launches pi itself.
15
7
  */
16
8
 
17
9
  import { spawn } from "node:child_process";
18
10
  import path from "node:path";
11
+ import type { Readable } from "node:stream";
19
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
13
 
21
14
  // Static package path, no user-controlled segments.
22
15
  const PISESH_CLI = path.resolve(__dirname, "../bin/pisesh"); // pi-lens-ignore: ts-path-traversal
16
+ const THINKING_LEVELS = [
17
+ "off",
18
+ "minimal",
19
+ "low",
20
+ "medium",
21
+ "high",
22
+ "xhigh",
23
+ "max",
24
+ ] as const;
25
+ type ThinkingLevel = (typeof THINKING_LEVELS)[number];
26
+
27
+ type PiseshSelection = {
28
+ version: 1;
29
+ sessionPath: string;
30
+ resumeMode: "defaults" | "session";
31
+ cwdOverride?: string;
32
+ model?: string;
33
+ thinking?: ThinkingLevel;
34
+ repaired?: number;
35
+ };
36
+
37
+ type PickerResult = {
38
+ code: number | null;
39
+ selection?: PiseshSelection;
40
+ error?: string;
41
+ };
42
+
43
+ type PendingSwitch = Pick<
44
+ PiseshSelection,
45
+ "sessionPath" | "cwdOverride" | "model" | "thinking" | "repaired"
46
+ >;
23
47
 
24
- function runPisesh(
25
- currentSessionId: string | undefined,
26
- ): Promise<number | null> {
48
+ const processState = globalThis as typeof globalThis & {
49
+ __piseshPendingSwitch?: PendingSwitch;
50
+ };
51
+
52
+ function parseSelection(raw: string): PiseshSelection | undefined {
53
+ if (!raw.trim()) return undefined;
54
+
55
+ let value: unknown;
56
+ try {
57
+ value = JSON.parse(raw);
58
+ } catch {
59
+ throw new Error("picker returned invalid JSON");
60
+ }
61
+ if (!value || typeof value !== "object") {
62
+ throw new Error("picker returned a non-object selection");
63
+ }
64
+ const data = value as Record<string, unknown>;
65
+ if (
66
+ data.version !== 1 ||
67
+ typeof data.sessionPath !== "string" ||
68
+ !path.isAbsolute(data.sessionPath) ||
69
+ (data.resumeMode !== "defaults" && data.resumeMode !== "session")
70
+ ) {
71
+ throw new Error("picker returned an invalid session selection");
72
+ }
73
+ if (data.cwdOverride !== undefined && typeof data.cwdOverride !== "string") {
74
+ throw new Error("picker returned an invalid cwd override");
75
+ }
76
+ if (data.model !== undefined && typeof data.model !== "string") {
77
+ throw new Error("picker returned an invalid model");
78
+ }
79
+ if (
80
+ data.thinking !== undefined &&
81
+ !THINKING_LEVELS.includes(data.thinking as ThinkingLevel)
82
+ ) {
83
+ throw new Error("picker returned an invalid thinking level");
84
+ }
85
+ if (
86
+ data.repaired !== undefined &&
87
+ (!Number.isInteger(data.repaired) || (data.repaired as number) < 0)
88
+ ) {
89
+ throw new Error("picker returned an invalid repair count");
90
+ }
91
+ return data as PiseshSelection;
92
+ }
93
+
94
+ function runPisesh(currentSessionId: string | undefined): Promise<PickerResult> {
27
95
  return new Promise((resolve) => {
28
- // stdio:"inherit" hands the real TTY to pisesh. pi's tui.stop() has
29
- // already detached so this is safe.
30
- // PISESH_CURRENT_SESSION lets pisesh flag the row that belongs to the
31
- // pi instance that just spawned it (rendered with a [NOW] badge).
96
+ let output = "";
97
+ let settled = false;
98
+ const finish = (result: PickerResult) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ resolve(result);
102
+ };
103
+
104
+ // fd 3 carries one small JSON result while stdin/stdout/stderr remain the
105
+ // real terminal used by the full-screen picker.
32
106
  const child = spawn("node", [PISESH_CLI], {
33
- stdio: "inherit",
107
+ stdio: ["inherit", "inherit", "inherit", "pipe"],
34
108
  env: {
35
109
  ...process.env,
36
- // Forward pi's cwd so pisesh's "Here" tab can show only the sessions
37
- // that belong to the directory this pi instance is attached to.
110
+ PISESH_SELECT_FD: "3",
38
111
  PISESH_CWD: process.cwd(),
39
112
  ...(currentSessionId
40
113
  ? { PISESH_CURRENT_SESSION: currentSessionId }
41
114
  : {}),
42
115
  },
43
116
  });
44
- child.on("exit", (code) => resolve(code));
45
- child.on("error", (err) => {
46
- // Surface a readable error in the terminal before we re-render.
117
+ const resultPipe = child.stdio[3] as Readable | null;
118
+ resultPipe?.setEncoding("utf8");
119
+ resultPipe?.on("data", (chunk: string) => {
120
+ output += chunk;
121
+ });
122
+ child.on("close", (code) => {
123
+ if (code !== 0) return finish({ code });
124
+ try {
125
+ finish({ code, selection: parseSelection(output) });
126
+ } catch (error) {
127
+ finish({
128
+ code,
129
+ error: error instanceof Error ? error.message : String(error),
130
+ });
131
+ }
132
+ });
133
+ child.on("error", (error) => {
47
134
  process.stdout.write(
48
- `\x1b[31mpisesh failed to launch: ${err.message}\x1b[0m\n`,
135
+ `\x1b[31mpisesh failed to launch: ${error.message}\x1b[0m\n`,
49
136
  );
50
- resolve(127);
137
+ finish({ code: 127, error: error.message });
51
138
  });
52
139
  });
53
140
  }
54
141
 
142
+ function sameSession(left: string | undefined, right: string): boolean {
143
+ return left ? path.resolve(left) === path.resolve(right) : false;
144
+ }
145
+
55
146
  export default function (pi: ExtensionAPI) {
147
+ // A successful switch loads a fresh extension instance before the old command
148
+ // returns. Plain pending data on globalThis lets that new instance apply the
149
+ // selected model and thinking without touching stale pre-switch pi/ctx objects.
150
+ pi.on("session_start", async (event, ctx) => {
151
+ const pending = processState.__piseshPendingSwitch;
152
+ if (
153
+ event.reason !== "resume" ||
154
+ !pending ||
155
+ !sameSession(ctx.sessionManager.getSessionFile(), pending.sessionPath)
156
+ ) {
157
+ return;
158
+ }
159
+ processState.__piseshPendingSwitch = undefined;
160
+
161
+ if (pending.model) {
162
+ const separator = pending.model.indexOf("/");
163
+ const model =
164
+ separator > 0
165
+ ? ctx.modelRegistry.find(
166
+ pending.model.slice(0, separator),
167
+ pending.model.slice(separator + 1),
168
+ )
169
+ : undefined;
170
+ if (!model || !(await pi.setModel(model))) {
171
+ ctx.ui.notify(
172
+ `Could not apply resume model: ${pending.model}`,
173
+ "warning",
174
+ );
175
+ }
176
+ }
177
+ if (pending.thinking) {
178
+ pi.setThinkingLevel(
179
+ pending.thinking as Parameters<typeof pi.setThinkingLevel>[0],
180
+ );
181
+ }
182
+ if (pending.repaired) {
183
+ ctx.ui.notify(
184
+ `Repaired ${pending.repaired} interrupted tool call${pending.repaired === 1 ? "" : "s"} before resume`,
185
+ "warning",
186
+ );
187
+ }
188
+ if (
189
+ pending.cwdOverride &&
190
+ path.resolve(ctx.cwd) !== path.resolve(pending.cwdOverride)
191
+ ) {
192
+ ctx.ui.notify(
193
+ "This pi version did not apply the selected cwd override; update pi to a version that supports it",
194
+ "warning",
195
+ );
196
+ }
197
+ });
198
+
56
199
  pi.registerCommand("sesh", {
57
200
  description: "Browse, star, and resume pi sessions (opens pisesh TUI)",
58
201
  handler: async (_args, ctx) => {
59
- if (!ctx.hasUI) {
60
- ctx.ui?.notify?.("/sesh requires interactive UI", "warning");
202
+ if (ctx.mode !== "tui") {
203
+ ctx.ui.notify("/sesh requires pi's interactive TUI", "warning");
61
204
  return;
62
205
  }
63
206
 
64
- let currentId: string | undefined;
65
- try {
66
- currentId = ctx.sessionManager?.getSessionId?.();
67
- } catch {
68
- currentId = undefined;
69
- }
70
-
71
- const code = await ctx.ui.custom<number | null>(
72
- (tui, _theme, _kb, done) => {
73
- // Hand over the terminal
207
+ const currentId = ctx.sessionManager.getSessionId();
208
+ const result = await ctx.ui.custom<PickerResult>(
209
+ (tui, _theme, _keybindings, done) => {
74
210
  tui.stop();
75
211
  process.stdout.write("\x1b[2J\x1b[H");
76
-
77
- runPisesh(currentId).then((exitCode) => {
78
- // Restore pi's TUI
212
+ void runPisesh(currentId).then((pickerResult) => {
79
213
  tui.start();
80
214
  tui.requestRender(true);
81
- done(exitCode);
215
+ done(pickerResult);
82
216
  });
83
-
84
- // Return a no-op component (custom() requires one synchronously)
85
217
  return { render: () => [], invalidate: () => {} };
86
218
  },
87
219
  );
88
220
 
89
- if (code === 0 || code === null) {
90
- ctx.ui.notify("Returned from pisesh", "info");
91
- } else if (code === 127) {
92
- ctx.ui.notify("pisesh failed to launch", "error");
93
- } else {
94
- ctx.ui.notify(`pisesh exited with code ${code}`, "warning");
221
+ if (!result) return;
222
+ if (result.error) {
223
+ ctx.ui.notify(`pisesh: ${result.error}`, "error");
224
+ return;
225
+ }
226
+ if (result.code !== 0 && result.code !== null) {
227
+ ctx.ui.notify(`pisesh exited with code ${result.code}`, "warning");
228
+ return;
229
+ }
230
+ const selection = result.selection;
231
+ if (!selection) return;
232
+
233
+ const currentFile = ctx.sessionManager.getSessionFile();
234
+ if (sameSession(currentFile, selection.sessionPath)) {
235
+ ctx.ui.notify("That session is already active", "info");
236
+ return;
237
+ }
238
+
239
+ const pending: PendingSwitch = {
240
+ sessionPath: selection.sessionPath,
241
+ cwdOverride: selection.cwdOverride,
242
+ model: selection.model,
243
+ thinking: selection.thinking,
244
+ repaired: selection.repaired,
245
+ };
246
+ processState.__piseshPendingSwitch = pending;
247
+
248
+ try {
249
+ const switchSession = ctx.switchSession as (
250
+ sessionPath: string,
251
+ options?: { cwdOverride?: string },
252
+ ) => Promise<{ cancelled: boolean }>;
253
+ const switched = await switchSession(
254
+ selection.sessionPath,
255
+ selection.cwdOverride
256
+ ? { cwdOverride: selection.cwdOverride }
257
+ : undefined,
258
+ );
259
+ if (switched.cancelled) {
260
+ processState.__piseshPendingSwitch = undefined;
261
+ ctx.ui.notify("Resume cancelled", "info");
262
+ }
263
+ } finally {
264
+ if (processState.__piseshPendingSwitch === pending) {
265
+ processState.__piseshPendingSwitch = undefined;
266
+ }
95
267
  }
96
268
  },
97
269
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pisesh",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Bookmark, search, and resume pi coding-agent sessions with a fast keyboard-driven TUI.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -27,7 +27,7 @@
27
27
  "author": "Blue-B <source_vs@naver.com>",
28
28
  "type": "commonjs",
29
29
  "bin": {
30
- "pisesh": "./bin/pisesh"
30
+ "pisesh": "bin/pisesh"
31
31
  },
32
32
  "files": [
33
33
  "bin/",