hatchee 0.1.1 → 0.1.3
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/dist/cli.mjs +261 -44
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11521,6 +11521,24 @@ class Daemon {
|
|
|
11521
11521
|
openPairingWindow(code, ttlMs = 5 * 60000) {
|
|
11522
11522
|
this.pairing = { code, expiresAt: Date.now() + ttlMs };
|
|
11523
11523
|
}
|
|
11524
|
+
pairingPayload(forceCode) {
|
|
11525
|
+
const code = forceCode || newPairingCode();
|
|
11526
|
+
this.openPairingWindow(code);
|
|
11527
|
+
const secret = b64.decode(this.cfg.secretKey);
|
|
11528
|
+
const pk = b64.encode(publicKey(secret));
|
|
11529
|
+
const room = b64url(publicKey(secret));
|
|
11530
|
+
const lan = lanIPv4();
|
|
11531
|
+
const payload = {
|
|
11532
|
+
v: 1,
|
|
11533
|
+
...lan ? { ws: `ws://${lan}:${this.cfg.wsPort}` } : {},
|
|
11534
|
+
code,
|
|
11535
|
+
host: this.cfg.host,
|
|
11536
|
+
pk,
|
|
11537
|
+
relay: this.cfg.relayUrl,
|
|
11538
|
+
room
|
|
11539
|
+
};
|
|
11540
|
+
return { payload, code, lan, room, relay: this.cfg.relayUrl, wsPort: this.cfg.wsPort };
|
|
11541
|
+
}
|
|
11524
11542
|
sealFor(p, msg) {
|
|
11525
11543
|
return p.key ? encode({ t: "enc", b: seal(p.key, encode(msg)) }) : encode(msg);
|
|
11526
11544
|
}
|
|
@@ -11923,7 +11941,18 @@ class Daemon {
|
|
|
11923
11941
|
sock.on("error", () => self.phones.delete(conn));
|
|
11924
11942
|
});
|
|
11925
11943
|
const hooks = createServer((req, res) => {
|
|
11926
|
-
|
|
11944
|
+
const url = req.url ?? "";
|
|
11945
|
+
if (req.method === "POST" && url === "/pair") {
|
|
11946
|
+
try {
|
|
11947
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
11948
|
+
res.end(JSON.stringify(self.pairingPayload()));
|
|
11949
|
+
} catch (e) {
|
|
11950
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
11951
|
+
res.end(JSON.stringify({ ok: false, error: String(e) }));
|
|
11952
|
+
}
|
|
11953
|
+
return;
|
|
11954
|
+
}
|
|
11955
|
+
if (req.method !== "POST" || url !== "/hook") {
|
|
11927
11956
|
res.writeHead(404).end("not found");
|
|
11928
11957
|
return;
|
|
11929
11958
|
}
|
|
@@ -11951,6 +11980,9 @@ class Daemon {
|
|
|
11951
11980
|
function decision(d, reason) {
|
|
11952
11981
|
return { body: { decision: d, reason } };
|
|
11953
11982
|
}
|
|
11983
|
+
function b64url(bytes) {
|
|
11984
|
+
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
11985
|
+
}
|
|
11954
11986
|
function toolSummary(toolName, input) {
|
|
11955
11987
|
switch (toolName) {
|
|
11956
11988
|
case "Bash":
|
|
@@ -11984,7 +12016,7 @@ class RelayClient {
|
|
|
11984
12016
|
this.secretKeyB64 = secretKeyB64;
|
|
11985
12017
|
}
|
|
11986
12018
|
get room() {
|
|
11987
|
-
return
|
|
12019
|
+
return b64url2(publicKey(b64.decode(this.secretKeyB64)));
|
|
11988
12020
|
}
|
|
11989
12021
|
url() {
|
|
11990
12022
|
return `${this.relayBase.replace(/\/$/, "")}/c/${this.room}?role=daemon`;
|
|
@@ -12065,7 +12097,7 @@ class RelayClient {
|
|
|
12065
12097
|
}
|
|
12066
12098
|
}
|
|
12067
12099
|
}
|
|
12068
|
-
function
|
|
12100
|
+
function b64url2(bytes) {
|
|
12069
12101
|
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12070
12102
|
}
|
|
12071
12103
|
|
|
@@ -12165,8 +12197,171 @@ async function runHook(hookPort) {
|
|
|
12165
12197
|
process.exit(0);
|
|
12166
12198
|
}
|
|
12167
12199
|
|
|
12200
|
+
// src/service.ts
|
|
12201
|
+
import { homedir as homedir3 } from "node:os";
|
|
12202
|
+
import { join as join3, dirname } from "node:path";
|
|
12203
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, copyFileSync, chmodSync, existsSync as existsSync3, rmSync } from "node:fs";
|
|
12204
|
+
import { execFileSync } from "node:child_process";
|
|
12205
|
+
var HOME = homedir3();
|
|
12206
|
+
var HATCHEE_DIR = join3(HOME, ".hatchee");
|
|
12207
|
+
var BIN_DIR = join3(HATCHEE_DIR, "bin");
|
|
12208
|
+
var STABLE_BIN = join3(BIN_DIR, "hatchee.mjs");
|
|
12209
|
+
var LOG = join3(HATCHEE_DIR, "daemon.log");
|
|
12210
|
+
var LABEL = "cloud.hatchee.daemon";
|
|
12211
|
+
var PLIST = join3(HOME, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
12212
|
+
var UNIT = join3(HOME, ".config", "systemd", "user", "hatchee.service");
|
|
12213
|
+
function servicePath(node) {
|
|
12214
|
+
return [
|
|
12215
|
+
dirname(node),
|
|
12216
|
+
join3(HOME, ".bun/bin"),
|
|
12217
|
+
join3(HOME, ".npm-global/bin"),
|
|
12218
|
+
join3(HOME, ".local/bin"),
|
|
12219
|
+
"/opt/homebrew/bin",
|
|
12220
|
+
"/usr/local/bin",
|
|
12221
|
+
"/usr/bin",
|
|
12222
|
+
"/bin",
|
|
12223
|
+
"/usr/sbin",
|
|
12224
|
+
"/sbin"
|
|
12225
|
+
].join(":");
|
|
12226
|
+
}
|
|
12227
|
+
function copyStableBin() {
|
|
12228
|
+
mkdirSync3(BIN_DIR, { recursive: true });
|
|
12229
|
+
const src = process.argv[1];
|
|
12230
|
+
if (src && src !== STABLE_BIN) {
|
|
12231
|
+
copyFileSync(src, STABLE_BIN);
|
|
12232
|
+
chmodSync(STABLE_BIN, 493);
|
|
12233
|
+
}
|
|
12234
|
+
}
|
|
12235
|
+
function plistContent(node, bin) {
|
|
12236
|
+
const args = [node, bin, "up"].map((s) => ` <string>${s}</string>`).join(`
|
|
12237
|
+
`);
|
|
12238
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12239
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
12240
|
+
<plist version="1.0">
|
|
12241
|
+
<dict>
|
|
12242
|
+
<key>Label</key><string>${LABEL}</string>
|
|
12243
|
+
<key>ProgramArguments</key>
|
|
12244
|
+
<array>
|
|
12245
|
+
${args}
|
|
12246
|
+
</array>
|
|
12247
|
+
<key>RunAtLoad</key><true/>
|
|
12248
|
+
<key>KeepAlive</key><true/>
|
|
12249
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
12250
|
+
<key>StandardOutPath</key><string>${LOG}</string>
|
|
12251
|
+
<key>StandardErrorPath</key><string>${LOG}</string>
|
|
12252
|
+
<key>EnvironmentVariables</key>
|
|
12253
|
+
<dict><key>PATH</key><string>${servicePath(node)}</string></dict>
|
|
12254
|
+
</dict>
|
|
12255
|
+
</plist>
|
|
12256
|
+
`;
|
|
12257
|
+
}
|
|
12258
|
+
function unitContent(node, bin) {
|
|
12259
|
+
return `[Unit]
|
|
12260
|
+
Description=Hatchee — watch & approve your coding agents from your phone
|
|
12261
|
+
After=network-online.target
|
|
12262
|
+
|
|
12263
|
+
[Service]
|
|
12264
|
+
ExecStart=${node} ${bin} up
|
|
12265
|
+
Restart=always
|
|
12266
|
+
RestartSec=10
|
|
12267
|
+
Environment=PATH=${servicePath(node)}
|
|
12268
|
+
|
|
12269
|
+
[Install]
|
|
12270
|
+
WantedBy=default.target
|
|
12271
|
+
`;
|
|
12272
|
+
}
|
|
12273
|
+
function run(cmd, args) {
|
|
12274
|
+
try {
|
|
12275
|
+
execFileSync(cmd, args, { stdio: "pipe" });
|
|
12276
|
+
} catch (e) {
|
|
12277
|
+
console.log(` · ${cmd} ${args.join(" ")} → ${String(e?.stderr || e?.message || e).trim().slice(0, 200)}`);
|
|
12278
|
+
}
|
|
12279
|
+
}
|
|
12280
|
+
function installService(print = false) {
|
|
12281
|
+
const node = process.execPath;
|
|
12282
|
+
if (process.platform === "darwin") {
|
|
12283
|
+
const content = plistContent(node, STABLE_BIN);
|
|
12284
|
+
if (print) {
|
|
12285
|
+
console.log(`
|
|
12286
|
+
would write ${PLIST}:
|
|
12287
|
+
|
|
12288
|
+
${content}
|
|
12289
|
+
bundle → ${STABLE_BIN}
|
|
12290
|
+
load → launchctl bootstrap gui/$(id -u) ${PLIST}
|
|
12291
|
+
`);
|
|
12292
|
+
return;
|
|
12293
|
+
}
|
|
12294
|
+
copyStableBin();
|
|
12295
|
+
mkdirSync3(dirname(PLIST), { recursive: true });
|
|
12296
|
+
writeFileSync3(PLIST, content);
|
|
12297
|
+
const uid = String(process.getuid?.() ?? "");
|
|
12298
|
+
run("launchctl", ["bootout", `gui/${uid}/${LABEL}`]);
|
|
12299
|
+
run("launchctl", ["bootstrap", `gui/${uid}`, PLIST]);
|
|
12300
|
+
run("launchctl", ["enable", `gui/${uid}/${LABEL}`]);
|
|
12301
|
+
console.log(`
|
|
12302
|
+
✓ Hatchee will now run in the background (launchd: ${LABEL})`);
|
|
12303
|
+
console.log(` logs ${LOG}`);
|
|
12304
|
+
console.log(` pair npx hatchee pair (one terminal, once — to add a phone)`);
|
|
12305
|
+
console.log(` stop npx hatchee uninstall-service`);
|
|
12306
|
+
console.log(`
|
|
12307
|
+
Don't also run \`hatchee up\` manually — the service is the single instance.
|
|
12308
|
+
`);
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
if (process.platform === "linux") {
|
|
12312
|
+
const content = unitContent(node, STABLE_BIN);
|
|
12313
|
+
if (print) {
|
|
12314
|
+
console.log(`
|
|
12315
|
+
would write ${UNIT}:
|
|
12316
|
+
|
|
12317
|
+
${content}
|
|
12318
|
+
bundle → ${STABLE_BIN}
|
|
12319
|
+
enable → systemctl --user enable --now hatchee.service (+ loginctl enable-linger)
|
|
12320
|
+
`);
|
|
12321
|
+
return;
|
|
12322
|
+
}
|
|
12323
|
+
copyStableBin();
|
|
12324
|
+
mkdirSync3(dirname(UNIT), { recursive: true });
|
|
12325
|
+
writeFileSync3(UNIT, content);
|
|
12326
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
12327
|
+
run("systemctl", ["--user", "enable", "--now", "hatchee.service"]);
|
|
12328
|
+
run("loginctl", ["enable-linger", process.env.USER ?? ""]);
|
|
12329
|
+
console.log(`
|
|
12330
|
+
✓ Hatchee will now run in the background (systemd --user: hatchee.service)`);
|
|
12331
|
+
console.log(` logs journalctl --user -u hatchee.service -f`);
|
|
12332
|
+
console.log(` pair npx hatchee pair`);
|
|
12333
|
+
console.log(` stop npx hatchee uninstall-service
|
|
12334
|
+
`);
|
|
12335
|
+
return;
|
|
12336
|
+
}
|
|
12337
|
+
console.log(`
|
|
12338
|
+
Auto-start service isn't supported on ${process.platform} yet.`);
|
|
12339
|
+
console.log(` Run \`npx hatchee up\` in a terminal you keep open (or use WSL on Windows).
|
|
12340
|
+
`);
|
|
12341
|
+
}
|
|
12342
|
+
function uninstallService() {
|
|
12343
|
+
if (process.platform === "darwin") {
|
|
12344
|
+
const uid = String(process.getuid?.() ?? "");
|
|
12345
|
+
run("launchctl", ["bootout", `gui/${uid}/${LABEL}`]);
|
|
12346
|
+
if (existsSync3(PLIST))
|
|
12347
|
+
rmSync(PLIST);
|
|
12348
|
+
console.log(` ✓ background service removed (launchd: ${LABEL})`);
|
|
12349
|
+
} else if (process.platform === "linux") {
|
|
12350
|
+
run("systemctl", ["--user", "disable", "--now", "hatchee.service"]);
|
|
12351
|
+
if (existsSync3(UNIT)) {
|
|
12352
|
+
rmSync(UNIT);
|
|
12353
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
12354
|
+
}
|
|
12355
|
+
console.log(` ✓ background service removed (systemd --user: hatchee.service)`);
|
|
12356
|
+
} else {
|
|
12357
|
+
console.log(` no background service on ${process.platform}.`);
|
|
12358
|
+
}
|
|
12359
|
+
if (existsSync3(STABLE_BIN))
|
|
12360
|
+
rmSync(STABLE_BIN);
|
|
12361
|
+
}
|
|
12362
|
+
|
|
12168
12363
|
// src/cli.ts
|
|
12169
|
-
var VERSION2 = "0.1.
|
|
12364
|
+
var VERSION2 = "0.1.3";
|
|
12170
12365
|
var cmd = process.argv[2] ?? "help";
|
|
12171
12366
|
switch (cmd) {
|
|
12172
12367
|
case "up": {
|
|
@@ -12197,39 +12392,19 @@ switch (cmd) {
|
|
|
12197
12392
|
}
|
|
12198
12393
|
case "pair": {
|
|
12199
12394
|
const cfg = loadConfig();
|
|
12395
|
+
const remote = await remotePair(cfg.hookPort);
|
|
12396
|
+
if (remote) {
|
|
12397
|
+
printPairing(remote, true);
|
|
12398
|
+
break;
|
|
12399
|
+
}
|
|
12200
12400
|
const daemon = new Daemon(cfg);
|
|
12201
12401
|
if (!safeListen(daemon))
|
|
12202
12402
|
break;
|
|
12203
12403
|
const relay = new RelayClient(daemon, cfg.relayUrl, cfg.secretKey);
|
|
12204
12404
|
relay.start();
|
|
12205
12405
|
installHooks(hatcheeBin());
|
|
12206
|
-
const
|
|
12207
|
-
|
|
12208
|
-
const myPub = b64.encode(publicKey(b64.decode(cfg.secretKey)));
|
|
12209
|
-
const lan = lanIPv4();
|
|
12210
|
-
const payload = JSON.stringify({
|
|
12211
|
-
v: 1,
|
|
12212
|
-
...lan ? { ws: `ws://${lan}:${cfg.wsPort}` } : {},
|
|
12213
|
-
code,
|
|
12214
|
-
host: cfg.host,
|
|
12215
|
-
pk: myPub,
|
|
12216
|
-
relay: cfg.relayUrl,
|
|
12217
|
-
room: relay.room
|
|
12218
|
-
});
|
|
12219
|
-
banner();
|
|
12220
|
-
import_qrcode_terminal.default.generate(payload, { small: true }, (qr) => {
|
|
12221
|
-
console.log(qr);
|
|
12222
|
-
console.log(` scan with the Hatchee iOS app \u2014 or enter manually:`);
|
|
12223
|
-
if (lan)
|
|
12224
|
-
console.log(` LAN address ${lan}:${cfg.wsPort}`);
|
|
12225
|
-
else
|
|
12226
|
-
console.log(` LAN address no LAN address (VPN?) \u2014 phone will use the relay`);
|
|
12227
|
-
console.log(` relay ${cfg.relayUrl}/c/${relay.room}?role=phone`);
|
|
12228
|
-
console.log(` code ${code} (valid 5 minutes, single use)
|
|
12229
|
-
`);
|
|
12230
|
-
console.log(` keeping the daemon running after pairing\u2026 (^C to stop)`);
|
|
12231
|
-
tips();
|
|
12232
|
-
});
|
|
12406
|
+
const info = daemon.pairingPayload(process.env.DROVER_PAIR_CODE);
|
|
12407
|
+
printPairing(info, false);
|
|
12233
12408
|
break;
|
|
12234
12409
|
}
|
|
12235
12410
|
case "hook":
|
|
@@ -12259,6 +12434,12 @@ switch (cmd) {
|
|
|
12259
12434
|
console.log(`revoked ${dev.name}`);
|
|
12260
12435
|
break;
|
|
12261
12436
|
}
|
|
12437
|
+
case "install-service":
|
|
12438
|
+
installService(process.argv[3] === "--print" || process.argv[3] === "--dry-run");
|
|
12439
|
+
break;
|
|
12440
|
+
case "uninstall-service":
|
|
12441
|
+
uninstallService();
|
|
12442
|
+
break;
|
|
12262
12443
|
case "uninstall-hooks":
|
|
12263
12444
|
uninstallHooks();
|
|
12264
12445
|
console.log("hatchee hooks removed from ~/.claude/settings.json");
|
|
@@ -12271,30 +12452,66 @@ switch (cmd) {
|
|
|
12271
12452
|
default:
|
|
12272
12453
|
banner();
|
|
12273
12454
|
console.log(` usage:
|
|
12274
|
-
hatchee pair
|
|
12275
|
-
hatchee up
|
|
12276
|
-
hatchee
|
|
12277
|
-
hatchee
|
|
12278
|
-
hatchee
|
|
12455
|
+
hatchee pair pair a phone \u2014 prints a QR + 6-digit code (do this first)
|
|
12456
|
+
hatchee up start watching (no new pairing) \u2014 installs Claude Code hooks
|
|
12457
|
+
hatchee install-service run in the background across logins/reboots (recommended)
|
|
12458
|
+
hatchee uninstall-service stop running in the background
|
|
12459
|
+
hatchee devices list paired phones
|
|
12460
|
+
hatchee revoke <id> revoke a lost phone
|
|
12461
|
+
hatchee uninstall-hooks remove Hatchee's hooks from ~/.claude/settings.json
|
|
12279
12462
|
hatchee version
|
|
12280
12463
|
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12464
|
+
keep it on npx hatchee install-service (so it survives closing the terminal)
|
|
12465
|
+
update npx hatchee@latest pair (npx always fetches the newest)
|
|
12466
|
+
uninstall npx hatchee uninstall-service && npx hatchee uninstall-hooks && rm -rf ~/.hatchee
|
|
12467
|
+
config ~/.hatchee/config.json (your daemon identity + paired devices)`);
|
|
12284
12468
|
}
|
|
12285
12469
|
function banner() {
|
|
12286
12470
|
console.log(`
|
|
12287
12471
|
\uD83D\uDC23 hatchee ${VERSION2} \u2014 your coding agents, alive on your lock screen
|
|
12288
12472
|
`);
|
|
12289
12473
|
}
|
|
12474
|
+
async function remotePair(hookPort) {
|
|
12475
|
+
try {
|
|
12476
|
+
const r = await fetch(`http://127.0.0.1:${hookPort}/pair`, { method: "POST", signal: AbortSignal.timeout(2500) });
|
|
12477
|
+
if (!r.ok)
|
|
12478
|
+
return null;
|
|
12479
|
+
const info = await r.json();
|
|
12480
|
+
return info?.payload ? info : null;
|
|
12481
|
+
} catch {
|
|
12482
|
+
return null;
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
function printPairing(info, viaService) {
|
|
12486
|
+
banner();
|
|
12487
|
+
import_qrcode_terminal.default.generate(JSON.stringify(info.payload), { small: true }, (qr) => {
|
|
12488
|
+
console.log(qr);
|
|
12489
|
+
console.log(` scan with the Hatchee iOS app \u2014 or enter manually:`);
|
|
12490
|
+
if (info.lan)
|
|
12491
|
+
console.log(` LAN address ${info.lan}:${info.wsPort}`);
|
|
12492
|
+
else
|
|
12493
|
+
console.log(` LAN address no LAN address (VPN?) \u2014 phone will use the relay`);
|
|
12494
|
+
console.log(` relay ${info.relay}/c/${info.room}?role=phone`);
|
|
12495
|
+
console.log(` code ${info.code} (valid 5 minutes, single use)
|
|
12496
|
+
`);
|
|
12497
|
+
if (viaService)
|
|
12498
|
+
console.log(` paired against the Hatchee already running here \u2014 it keeps running.
|
|
12499
|
+
`);
|
|
12500
|
+
else {
|
|
12501
|
+
console.log(` keeping the daemon running after pairing\u2026 (^C to stop)`);
|
|
12502
|
+
tips();
|
|
12503
|
+
}
|
|
12504
|
+
});
|
|
12505
|
+
}
|
|
12290
12506
|
function tips() {
|
|
12291
12507
|
console.log(`
|
|
12292
12508
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12509
|
+
keep it on npx hatchee install-service (survives closing the terminal / reboot)
|
|
12510
|
+
stop ^C (your coding agents keep running \u2014 you just stop watching)
|
|
12511
|
+
update npx hatchee@latest pair
|
|
12512
|
+
uninstall npx hatchee uninstall-service && npx hatchee uninstall-hooks
|
|
12513
|
+
rm -rf ~/.hatchee (also wipes pairing + identity)
|
|
12514
|
+
help npx hatchee
|
|
12298
12515
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
12299
12516
|
}
|
|
12300
12517
|
function safeListen(daemon) {
|
package/package.json
CHANGED