gentle-pi 3.4.0 → 3.5.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.
@@ -0,0 +1,482 @@
1
+ import { join } from "node:path";
2
+
3
+ // The gentle-shell launcher: pure, side-effect-free functions over injected
4
+ // env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
5
+ // filesystem and child process so this module stays fully unit-testable.
6
+
7
+ export type LauncherCommand = "home";
8
+
9
+ // pi's own package-management subcommands (see pi's cli/args.ts printHelp
10
+ // "Commands" list): each is dispatched by pi itself, before pi's own flag
11
+ // parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
12
+ export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] as const;
13
+
14
+ export type PiSubcommand = (typeof PI_SUBCOMMANDS)[number];
15
+
16
+ function isPiSubcommand(token: string): token is PiSubcommand {
17
+ return (PI_SUBCOMMANDS as readonly string[]).includes(token);
18
+ }
19
+
20
+ export interface ParsedLauncherArgs {
21
+ link: boolean;
22
+ isolated: boolean;
23
+ home?: string;
24
+ help: boolean;
25
+ version: boolean;
26
+ command?: LauncherCommand;
27
+ commandArgs: string[];
28
+ passthrough: string[];
29
+ // Set when the first passthrough token is one of PI_SUBCOMMANDS (e.g.
30
+ // `gentle-shell install npm:x`). It stays part of `passthrough` — this
31
+ // field only tells buildPiInvocation to skip its extension injection, so
32
+ // pi sees the bare subcommand it expects as argv[0].
33
+ piSubcommand?: PiSubcommand;
34
+ error?: string;
35
+ }
36
+
37
+ // Home-subcommand parsing is deliberately shallow: `home` only counts as the
38
+ // subcommand when it is argv[0], and everything after it is handed over
39
+ // untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
40
+ export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
41
+ if (argv[0] === "home") {
42
+ return {
43
+ link: false,
44
+ isolated: false,
45
+ home: undefined,
46
+ help: false,
47
+ version: false,
48
+ command: "home",
49
+ commandArgs: argv.slice(1),
50
+ passthrough: [],
51
+ piSubcommand: undefined,
52
+ error: undefined,
53
+ };
54
+ }
55
+
56
+ let link = false;
57
+ let isolated = false;
58
+ let home: string | undefined;
59
+ let help = false;
60
+ let version = false;
61
+ let error: string | undefined;
62
+ let piSubcommand: PiSubcommand | undefined;
63
+ const passthrough: string[] = [];
64
+
65
+ for (let i = 0; i < argv.length; i += 1) {
66
+ const arg = argv[i];
67
+ if (arg === "--") {
68
+ const rest = argv.slice(i + 1);
69
+ if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
70
+ piSubcommand = rest[0];
71
+ }
72
+ passthrough.push(...rest);
73
+ break;
74
+ }
75
+ if (arg === "--link") {
76
+ link = true;
77
+ continue;
78
+ }
79
+ if (arg === "--isolated") {
80
+ isolated = true;
81
+ continue;
82
+ }
83
+ if (arg === "--help" || arg === "-h") {
84
+ help = true;
85
+ continue;
86
+ }
87
+ if (arg === "--version") {
88
+ version = true;
89
+ continue;
90
+ }
91
+ if (arg.startsWith("--home=")) {
92
+ const value = arg.slice("--home=".length);
93
+ if (value.length === 0) {
94
+ error = "--home requires a non-empty path argument";
95
+ continue;
96
+ }
97
+ home = value;
98
+ continue;
99
+ }
100
+ if (arg === "--home") {
101
+ const value = argv[i + 1];
102
+ if (value === undefined || value.length === 0) {
103
+ error = "--home requires a non-empty path argument";
104
+ if (value !== undefined) i += 1;
105
+ continue;
106
+ }
107
+ home = value;
108
+ i += 1;
109
+ continue;
110
+ }
111
+ if (passthrough.length === 0 && isPiSubcommand(arg)) {
112
+ piSubcommand = arg;
113
+ }
114
+ passthrough.push(arg);
115
+ }
116
+
117
+ if (error === undefined) {
118
+ if (link && isolated) {
119
+ error = "--link cannot be combined with --isolated";
120
+ } else if (link && home !== undefined) {
121
+ error = "--link cannot be combined with --home";
122
+ } else if (isolated && home !== undefined) {
123
+ error = "--isolated cannot be combined with --home";
124
+ }
125
+ }
126
+
127
+ return { link, isolated, home, help, version, command: undefined, commandArgs: [], passthrough, piSubcommand, error };
128
+ }
129
+
130
+ // --- home resolution -------------------------------------------------------
131
+
132
+ export type HomeMode = "link" | "isolated" | "path";
133
+ export type HomeSource = "flag" | "config" | "default";
134
+
135
+ export interface ResolvedHome {
136
+ mode: HomeMode;
137
+ dir: string;
138
+ source: HomeSource;
139
+ }
140
+
141
+ // A discriminated union instead of a plain `home: string` field: `resolveHome`
142
+ // switches on `mode` rather than re-parsing the raw on-disk string, and the
143
+ // `path` case carries its `dir` explicitly so a "link"/"isolated" string can
144
+ // never be mistaken for a filesystem path at the call site.
145
+ export type LauncherConfig = { mode: "link" } | { mode: "isolated" } | { mode: "path"; dir: string };
146
+
147
+ export interface ResolveHomeInput {
148
+ args: ParsedLauncherArgs;
149
+ env: Record<string, string | undefined>;
150
+ homedir: string;
151
+ config: LauncherConfig | undefined;
152
+ }
153
+
154
+ // Pi Subagents resolves `PI_CODING_AGENT_DIR || ~/.pi/agent`; `--link` reuses
155
+ // that exact home so gentle-shell never diverges from the user's own pi.
156
+ function linkDir(env: Record<string, string | undefined>, homedir: string): string {
157
+ return env.PI_CODING_AGENT_DIR || join(homedir, ".pi", "agent");
158
+ }
159
+
160
+ function isolatedDir(env: Record<string, string | undefined>, homedir: string): string {
161
+ return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
162
+ }
163
+
164
+ export function resolveHome(input: ResolveHomeInput): ResolvedHome {
165
+ const { args, env, homedir, config } = input;
166
+
167
+ if (args.link) return { mode: "link", dir: linkDir(env, homedir), source: "flag" };
168
+ if (args.isolated) return { mode: "isolated", dir: isolatedDir(env, homedir), source: "flag" };
169
+ if (args.home !== undefined) return { mode: "path", dir: args.home, source: "flag" };
170
+
171
+ if (config !== undefined) {
172
+ if (config.mode === "link") return { mode: "link", dir: linkDir(env, homedir), source: "config" };
173
+ if (config.mode === "isolated") return { mode: "isolated", dir: isolatedDir(env, homedir), source: "config" };
174
+ return { mode: "path", dir: config.dir, source: "config" };
175
+ }
176
+
177
+ return { mode: "isolated", dir: isolatedDir(env, homedir), source: "default" };
178
+ }
179
+
180
+ export function launcherConfigPath(homedir: string): string {
181
+ return join(homedir, ".gentle-shell", "config.json");
182
+ }
183
+
184
+ // Tolerant on purpose: a malformed or foreign config.json must never crash
185
+ // the launcher, it just falls through to the default isolated home.
186
+ //
187
+ // The on-disk shape stays the flat `{ "home": "link" | "isolated" | "<path>" }`
188
+ // documented in the feature scope; only the parsed, in-memory `LauncherConfig`
189
+ // is a discriminated union. Any non-empty string other than the exact literals
190
+ // "link" or "isolated" is treated as a path, including a near-miss like
191
+ // "linked" — this is deliberate: there is no separate "unrecognised mode"
192
+ // error, a typo just resolves to a (probably nonexistent) path instead.
193
+ export function parseLauncherConfig(text: string): LauncherConfig | undefined {
194
+ let parsed: unknown;
195
+ try {
196
+ parsed = JSON.parse(text);
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
201
+ const home = (parsed as Record<string, unknown>).home;
202
+ if (typeof home !== "string" || home.length === 0) return undefined;
203
+ if (home === "link") return { mode: "link" };
204
+ if (home === "isolated") return { mode: "isolated" };
205
+ return { mode: "path", dir: home };
206
+ }
207
+
208
+ // --- pi runtime resolution ---------------------------------------------------
209
+
210
+ export type PiRuntimeKind = "env" | "bundled" | "path";
211
+
212
+ export interface PiRuntime {
213
+ kind: PiRuntimeKind;
214
+ command: string;
215
+ args: string[];
216
+ }
217
+
218
+ export interface PiRuntimeDeps {
219
+ env: Record<string, string | undefined>;
220
+ resolveBundledCli: () => string | undefined;
221
+ findOnPath: (name: string) => string | undefined;
222
+ nodeExecPath: string;
223
+ }
224
+
225
+ export function resolvePiRuntime(deps: PiRuntimeDeps): PiRuntime | undefined {
226
+ const envOverride = deps.env.GENTLE_SHELL_PI;
227
+ if (envOverride !== undefined && envOverride.length > 0) return { kind: "env", command: envOverride, args: [] };
228
+
229
+ const bundledCliPath = deps.resolveBundledCli();
230
+ if (bundledCliPath !== undefined) return { kind: "bundled", command: deps.nodeExecPath, args: [bundledCliPath] };
231
+
232
+ const onPath = deps.findOnPath("pi");
233
+ if (onPath !== undefined) return { kind: "path", command: onPath, args: [] };
234
+
235
+ return undefined;
236
+ }
237
+
238
+ export function missingPiMessage(): string {
239
+ return [
240
+ "No pi runtime could be found. Pick one of:",
241
+ " - Set GENTLE_SHELL_PI to the path of a pi executable.",
242
+ " - Install @earendil-works/pi-coding-agent next to gentle-pi (it ships as an optional peer dependency).",
243
+ " - Install pi and make sure it is on your PATH.",
244
+ ].join("\n");
245
+ }
246
+
247
+ // --- pi version gate ---------------------------------------------------------
248
+
249
+ export const MIN_PI_VERSION = "0.85.1";
250
+
251
+ export type PiVersionCheck = { ok: true; version: string } | { ok: false; message: string; version?: string };
252
+
253
+ const VERSION_PATTERN = /v?(\d+)\.(\d+)\.(\d+)/;
254
+
255
+ function compareVersions(a: readonly [number, number, number], b: readonly [number, number, number]): number {
256
+ for (let i = 0; i < 3; i += 1) {
257
+ if (a[i] !== b[i]) return a[i] - b[i];
258
+ }
259
+ return 0;
260
+ }
261
+
262
+ export function checkPiVersion(output: string, minimum: string = MIN_PI_VERSION): PiVersionCheck {
263
+ const match = VERSION_PATTERN.exec(output);
264
+ if (!match) {
265
+ return { ok: false, message: `Could not determine the pi version from "${output.trim()}" (need at least ${minimum}).` };
266
+ }
267
+ const version = `${match[1]}.${match[2]}.${match[3]}`;
268
+ const minimumMatch = VERSION_PATTERN.exec(minimum);
269
+ if (!minimumMatch) throw new Error(`invalid minimum version "${minimum}"`);
270
+ const found: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];
271
+ const wanted: [number, number, number] = [Number(minimumMatch[1]), Number(minimumMatch[2]), Number(minimumMatch[3])];
272
+ if (compareVersions(found, wanted) < 0) {
273
+ return { ok: false, version, message: `pi version ${version} is older than the required minimum ${minimum}.` };
274
+ }
275
+ return { ok: true, version };
276
+ }
277
+
278
+ // --- packaging drift guard -----------------------------------------------------
279
+
280
+ export interface PackageJsonPeerShape {
281
+ peerDependencies?: Record<string, string>;
282
+ }
283
+
284
+ export type PeerVersionPinCheck = { ok: true; pinned: string } | { ok: false; message: string };
285
+
286
+ // Keeps the MIN_PI_VERSION drift-guard test's failure readable: a missing
287
+ // peerDependencies block, a missing peer entry, or a malformed range must
288
+ // fail with a clear assertion message, not a raw TypeError from indexing an
289
+ // undefined value the way a direct `packageJson.peerDependencies[peerName]`
290
+ // lookup would.
291
+ export function checkPeerVersionPin(packageJson: PackageJsonPeerShape, peerName: string, minVersion: string): PeerVersionPinCheck {
292
+ const peerDependencies = packageJson.peerDependencies;
293
+ if (peerDependencies === undefined) {
294
+ return { ok: false, message: "package.json is missing a peerDependencies block" };
295
+ }
296
+ const pinned = peerDependencies[peerName];
297
+ if (typeof pinned !== "string") {
298
+ return { ok: false, message: `package.json peerDependencies is missing "${peerName}"` };
299
+ }
300
+ if (!/^>=\d+\.\d+\.\d+$/.test(pinned)) {
301
+ return { ok: false, message: `package.json peerDependencies["${peerName}"] ("${pinned}") is not a simple >=x.y.z range` };
302
+ }
303
+ const version = pinned.replace(/^>=/, "");
304
+ if (version !== minVersion) {
305
+ return { ok: false, message: `MIN_PI_VERSION ("${minVersion}") does not match the pinned peer range ("${pinned}")` };
306
+ }
307
+ return { ok: true, pinned };
308
+ }
309
+
310
+ // --- settings.json detection ---------------------------------------------------
311
+
312
+ function packageEntryDeclaresGentlePi(entry: unknown): boolean {
313
+ const declares = (value: unknown): boolean => typeof value === "string" && (value === "npm:gentle-pi" || value.startsWith("npm:gentle-pi@"));
314
+ if (declares(entry)) return true;
315
+ if (entry !== null && typeof entry === "object") return declares((entry as Record<string, unknown>).source);
316
+ return false;
317
+ }
318
+
319
+ export function settingsDeclareGentlePi(settingsText: string | undefined): boolean {
320
+ if (settingsText === undefined) return false;
321
+ let parsed: unknown;
322
+ try {
323
+ parsed = JSON.parse(settingsText);
324
+ } catch {
325
+ return false;
326
+ }
327
+ if (typeof parsed !== "object" || parsed === null) return false;
328
+ const packages = (parsed as Record<string, unknown>).packages;
329
+ if (!Array.isArray(packages)) return false;
330
+ return packages.some(packageEntryDeclaresGentlePi);
331
+ }
332
+
333
+ // --- pi invocation builder ---------------------------------------------------
334
+
335
+ export interface BuildPiInvocationInput {
336
+ runtime: PiRuntime;
337
+ home: ResolvedHome;
338
+ packageRoot: string;
339
+ settingsDeclareGentlePi: boolean;
340
+ passthrough: string[];
341
+ // Set when parseLauncherArgs recognised passthrough[0] as one of
342
+ // PI_SUBCOMMANDS. pi dispatches install/remove/uninstall/update/list/
343
+ // config/auth on argv[0] before its own flag parsing, so none of the
344
+ // gentle-pi extension injection below may precede it.
345
+ piSubcommand?: PiSubcommand;
346
+ baseEnv: Record<string, string | undefined>;
347
+ }
348
+
349
+ export interface PiInvocation {
350
+ command: string;
351
+ args: string[];
352
+ env: Record<string, string | undefined>;
353
+ }
354
+
355
+ // The `-e/--theme/--skill/--prompt-template` injection is skipped when the
356
+ // caller already confirmed the target settings.json declares the package
357
+ // (the `--link` case with a pi-managed install), or when passthrough[0] is
358
+ // one of pi's own subcommands: pi dispatches install/remove/uninstall/
359
+ // update/list/config/auth on argv[0] before it even parses flags, so any
360
+ // injected flag ahead of it stops pi from recognising its subcommand at
361
+ // all — this is exactly the observed 2026-09-22 bug where `gentle-shell
362
+ // install npm:x` opened an interactive pi session instead of running the
363
+ // package manager. Isolated and path homes never declare the package, so
364
+ // callers pass `settingsDeclareGentlePi: false` for those and the
365
+ // injection always happens there, unless a pi subcommand is set.
366
+ export function buildPiInvocation(input: BuildPiInvocationInput): PiInvocation {
367
+ const args = [...input.runtime.args];
368
+ if (input.piSubcommand === undefined && !input.settingsDeclareGentlePi) {
369
+ args.push(
370
+ "-e",
371
+ input.packageRoot,
372
+ "--theme",
373
+ join(input.packageRoot, "themes"),
374
+ "--skill",
375
+ join(input.packageRoot, "skills"),
376
+ "--prompt-template",
377
+ join(input.packageRoot, "prompts"),
378
+ );
379
+ }
380
+ args.push(...input.passthrough);
381
+
382
+ return {
383
+ command: input.runtime.command,
384
+ args,
385
+ env: { ...input.baseEnv, PI_CODING_AGENT_DIR: input.home.dir, GENTLE_PI_AGENT_HOME: input.home.dir },
386
+ };
387
+ }
388
+
389
+ // --- spawn planning ------------------------------------------------------------
390
+
391
+ // R3-001: `findOnPath` can resolve a PATHEXT candidate such as a .CMD or .BAT
392
+ // shim on win32 (exactly how an npm-installed `pi` lands on PATH), and a
393
+ // GENTLE_SHELL_PI override can point at one too. Current Node releases refuse
394
+ // to spawn a batch file directly without `shell: true` (EINVAL), so both the
395
+ // version probe and the real launch route a batch shim through cmd.exe as one
396
+ // quoted command line instead of spawning it directly.
397
+ const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
398
+
399
+ // cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
400
+ // token in double quotes when it is empty or contains whitespace or any of
401
+ // `"&|<>^%()`, and escape an inner `"` as `\"` — doubling inner quotes is not
402
+ // reliable in cmd.exe, unlike the `\"` convention Node's own Windows spawn
403
+ // helpers use.
404
+ export function quoteForCmdExe(token: string): string {
405
+ if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
406
+ return `"${token.replace(/"/g, '\\"')}"`;
407
+ }
408
+
409
+ export interface PlanSpawnInput {
410
+ command: string;
411
+ args: string[];
412
+ platform: NodeJS.Platform;
413
+ }
414
+
415
+ export interface SpawnPlan {
416
+ command: string;
417
+ args: string[];
418
+ shell: boolean;
419
+ }
420
+
421
+ export function planSpawn(input: PlanSpawnInput): SpawnPlan {
422
+ const { command, args, platform } = input;
423
+ if (platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
424
+ return { command: [command, ...args].map(quoteForCmdExe).join(" "), args: [], shell: true };
425
+ }
426
+ return { command, args, shell: false };
427
+ }
428
+
429
+ // --- reporting ---------------------------------------------------------------
430
+
431
+ export interface DescribeVersionInput {
432
+ gentlePiVersion: string;
433
+ piVersion: string | undefined;
434
+ home: ResolvedHome;
435
+ }
436
+
437
+ export function describeVersion(input: DescribeVersionInput): string {
438
+ return [
439
+ `gentle-shell ${input.gentlePiVersion}`,
440
+ `pi ${input.piVersion ?? "not found"}`,
441
+ `home ${input.home.mode} ${input.home.dir}`,
442
+ ].join("\n");
443
+ }
444
+
445
+ export function helpText(): string {
446
+ return [
447
+ "Usage: gentle-shell [options] [-- pi-args...]",
448
+ " gentle-shell home [link|isolated|<path>]",
449
+ "",
450
+ "Opens pi with the Gentle Shell package loaded, without touching your",
451
+ "vanilla pi installation.",
452
+ "",
453
+ "Options:",
454
+ " --link Use your existing pi agent home (never edits its settings.json).",
455
+ " --isolated Use the dedicated ~/.gentle-shell/agent home (default).",
456
+ " --home <path> Use a custom agent home directory.",
457
+ " --help, -h Show this help text.",
458
+ " --version Show gentle-shell, pi, and home version information.",
459
+ "",
460
+ "Commands:",
461
+ " home Print or persist the effective home mode (link, isolated, or a path).",
462
+ "",
463
+ "Managing packages:",
464
+ " gentle-shell install npm:<pkg> Run pi's own 'install' against the resolved home.",
465
+ " gentle-shell remove <source> Run pi's own 'remove' against the resolved home.",
466
+ " gentle-shell list Run pi's own 'list' against the resolved home.",
467
+ " gentle-shell update [target] Run pi's own 'update' against the resolved home.",
468
+ " gentle-shell config Run pi's own 'config' against the resolved home.",
469
+ " gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
470
+ " These run pi's own commands, forwarded verbatim, against the --isolated home",
471
+ " (or your own pi home with --link). Running 'gentle-shell install npm:gentle-pi'",
472
+ " inside the isolated home is unnecessary: gentle-shell already loads the",
473
+ " package itself.",
474
+ "",
475
+ "Environment variables:",
476
+ " GENTLE_SHELL_PI Path to the pi executable to run.",
477
+ " GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent).",
478
+ " PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
479
+ "",
480
+ "Every other argument is forwarded to pi unchanged.",
481
+ ].join("\n");
482
+ }
@@ -0,0 +1,36 @@
1
+ // Interactive RPC host signal: the desktop app sets this environment variable
2
+ // on the pi process it spawns directly (`--mode rpc`), letting Gentle answer
3
+ // ask-user tools through pi's RPC dialogs and publish live subagent state.
4
+ // `lib/agents-runner.ts` strips it from every subagent child env so headless
5
+ // RPC children never see it and stay unaffected by this feature.
6
+
7
+ /** Environment variable the desktop app sets on its own interactive pi process. */
8
+ export const INTERACTIVE_HOST_ENV = "GENTLE_SHELL_INTERACTIVE_HOST";
9
+
10
+ /**
11
+ * True only when running under `--mode rpc` with the interactive host
12
+ * variable set to exactly `"1"`. Any other value, or its absence, keeps RPC
13
+ * headless (the existing subagent behaviour).
14
+ */
15
+ export function isInteractiveRpcHost(mode: string, env: NodeJS.ProcessEnv = process.env): boolean {
16
+ return mode === "rpc" && env[INTERACTIVE_HOST_ENV] === "1";
17
+ }
18
+
19
+ /**
20
+ * True for the interactive TUI, or for an interactive RPC host. Extensions
21
+ * use this to offer dialog-capable behaviour without special-casing RPC.
22
+ */
23
+ export function isInteractiveMode(mode: string, env: NodeJS.ProcessEnv = process.env): boolean {
24
+ return mode === "tui" || isInteractiveRpcHost(mode, env);
25
+ }
26
+
27
+ /**
28
+ * Copy of `env` with the interactive host variable removed. Used when
29
+ * assembling a subagent child's environment so it never inherits the
30
+ * parent's interactive-host signal, even when the parent itself is one.
31
+ */
32
+ export function withoutInteractiveHost(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
33
+ const next = { ...env };
34
+ delete next[INTERACTIVE_HOST_ENV];
35
+ return next;
36
+ }
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "bin": {
8
+ "gentle-shell": "bin/gentle-shell.mjs"
9
+ },
7
10
  "keywords": [
8
11
  "pi-package",
9
12
  "pi",
@@ -24,6 +27,7 @@
24
27
  },
25
28
  "files": [
26
29
  "assets/",
30
+ "bin/",
27
31
  "contracts/",
28
32
  "docs/",
29
33
  "extensions/",