@rubytech/create-maxy-code 0.1.305 → 0.1.306

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.305",
3
+ "version": "0.1.306",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:1029bfc516fc6bed4265dcaffd6068d7ac72674d4e066c15c64e8c19df744b5e
4
+ content-hash: sha256:9d8f9a898ee1cb0726e604e78f5fd1bf2c9660b76604d523ba1c4e3598092a98
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -41,6 +41,19 @@ Because Maxy runs locally, your data stays in your home. It never passes through
41
41
 
42
42
  **Public (visitors)** — anyone who reaches your public URL gets the public agent. It handles product enquiries, collects prospect details, and answers questions about your business. It cannot read or write your private data.
43
43
 
44
+ ## Install the Dashboard as an App
45
+
46
+ The admin dashboard and the chat dashboard can be installed as standalone apps, so they open in their own window with their own icon instead of a browser tab. Installing changes nothing about how Maxy works — it is the same dashboard, just launched like any other app.
47
+
48
+ **On a phone or tablet:**
49
+
50
+ - **iPhone or iPad (Safari):** open the dashboard, tap the Share button, then **Add to Home Screen**. A Maxy icon appears on your home screen; tapping it opens the dashboard full-screen.
51
+ - **Android (Chrome):** open the dashboard, then either tap the **Install** prompt Chrome offers, or open the browser menu and choose **Install app** / **Add to Home screen**.
52
+
53
+ **On a computer (Chrome or Edge):** open the dashboard, then click the **Install** icon at the right of the address bar (or open the browser menu and choose **Install**). The dashboard opens in its own window from then on.
54
+
55
+ The admin dashboard and the chat dashboard install as two separate apps, so you can keep both on your home screen or dock. Each one opens straight to its own screen.
56
+
44
57
  ## First Power-On (New Device)
45
58
 
46
59
  If your device has no WiFi configured and no ethernet cable connected, it creates a temporary WiFi network for setup:
@@ -14,6 +14,19 @@ Because {{productName}} runs locally, your data stays in your home. It never pas
14
14
 
15
15
  **Public (visitors)** — anyone who reaches your public URL gets the public agent. It handles product enquiries, collects prospect details, and answers questions about your business. It cannot read or write your private data.
16
16
 
17
+ ## Install the Dashboard as an App
18
+
19
+ The admin dashboard and the chat dashboard can be installed as standalone apps, so they open in their own window with their own icon instead of a browser tab. Installing changes nothing about how {{productName}} works — it is the same dashboard, just launched like any other app.
20
+
21
+ **On a phone or tablet:**
22
+
23
+ - **iPhone or iPad (Safari):** open the dashboard, tap the Share button, then **Add to Home Screen**. A {{productName}} icon appears on your home screen; tapping it opens the dashboard full-screen.
24
+ - **Android (Chrome):** open the dashboard, then either tap the **Install** prompt Chrome offers, or open the browser menu and choose **Install app** / **Add to Home screen**.
25
+
26
+ **On a computer (Chrome or Edge):** open the dashboard, then click the **Install** icon at the right of the address bar (or open the browser menu and choose **Install**). The dashboard opens in its own window from then on.
27
+
28
+ The admin dashboard and the chat dashboard install as two separate apps, so you can keep both on your home screen or dock. Each one opens straight to its own screen.
29
+
17
30
  ## First Power-On (New Device)
18
31
 
19
32
  If your device has no WiFi configured and no ethernet cable connected, it creates a temporary WiFi network for setup:
@@ -0,0 +1,24 @@
1
+ // Minimal installable-PWA service worker for the admin, operator, and chat
2
+ // shells. Its only job is to exist: browsers require a registered service
3
+ // worker with a fetch handler before they offer "Install" / "Add to Home
4
+ // Screen". It does NOT cache — no precaching, no offline app-shell, no runtime
5
+ // cache. Offline support is a separate, out-of-scope capability.
6
+ //
7
+ // Served at the origin root (/sw.js) so its default scope is '/', covering
8
+ // every surface. See app/lib/pwa.ts and .docs/pwa-installable-shells.md.
9
+
10
+ self.addEventListener('install', () => {
11
+ // Activate this worker immediately instead of waiting for existing tabs to
12
+ // close, so a freshly-installed app controls its pages on first launch.
13
+ self.skipWaiting()
14
+ })
15
+
16
+ self.addEventListener('activate', (event) => {
17
+ event.waitUntil(self.clients.claim())
18
+ })
19
+
20
+ self.addEventListener('fetch', () => {
21
+ // Network passthrough: no respondWith, so the browser handles every request
22
+ // exactly as it would without a service worker. Present only to satisfy the
23
+ // installability requirement.
24
+ })
@@ -1227,6 +1227,89 @@ import { resolve as resolve29, join as join22, basename as basename8 } from "pat
1227
1227
  import { homedir as homedir3 } from "os";
1228
1228
  import { monitorEventLoopDelay } from "perf_hooks";
1229
1229
 
1230
+ // app/lib/pwa.ts
1231
+ var PWA_SURFACES = [
1232
+ {
1233
+ surface: "admin",
1234
+ shellFile: "index.html",
1235
+ manifestPath: "/manifest-admin.webmanifest",
1236
+ nameSuffix: "",
1237
+ startUrl: "/",
1238
+ scope: "/",
1239
+ id: "/?pwa=admin"
1240
+ },
1241
+ {
1242
+ surface: "operator",
1243
+ shellFile: "operator.html",
1244
+ manifestPath: "/manifest-operator.webmanifest",
1245
+ nameSuffix: " Operator",
1246
+ startUrl: "/",
1247
+ scope: "/",
1248
+ id: "/?pwa=operator"
1249
+ },
1250
+ {
1251
+ surface: "chat",
1252
+ shellFile: "chat.html",
1253
+ manifestPath: "/manifest-chat.webmanifest",
1254
+ nameSuffix: " Chat",
1255
+ startUrl: "/chat",
1256
+ scope: "/chat",
1257
+ id: "/chat?pwa=chat"
1258
+ }
1259
+ ];
1260
+ var MANIFEST_CONTENT_TYPE = "application/manifest+json";
1261
+ var SW_CONTENT_TYPE = "application/javascript";
1262
+ function pwaSurfaceForShell(file) {
1263
+ return PWA_SURFACES.find((s) => s.shellFile === file);
1264
+ }
1265
+ function buildManifest(s, b) {
1266
+ const name = `${b.productName}${s.nameSuffix}`;
1267
+ return {
1268
+ id: s.id,
1269
+ name,
1270
+ short_name: name,
1271
+ start_url: s.startUrl,
1272
+ scope: s.scope,
1273
+ display: "standalone",
1274
+ theme_color: b.themeColor,
1275
+ background_color: b.backgroundColor,
1276
+ icons: [{ src: b.iconPath, sizes: "any", purpose: "any" }]
1277
+ };
1278
+ }
1279
+ function escAttr(s) {
1280
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1281
+ }
1282
+ function swRegisterScript(s) {
1283
+ return [
1284
+ "<script>",
1285
+ "if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(e){",
1286
+ `try{fetch('/api/_client-error',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({kind:'event',source:'pwa',op:'sw-register-fail',surface:'${s.surface}',scope:'${s.scope}',err:String((e&&e.message)||e)}),keepalive:true}).catch(function(){});}catch(_){}`,
1287
+ "});}",
1288
+ "</script>"
1289
+ ].join("\n");
1290
+ }
1291
+ function pwaHeadTags(s, b) {
1292
+ return [
1293
+ `<link rel="manifest" href="${escAttr(s.manifestPath)}">`,
1294
+ `<link rel="apple-touch-icon" href="${escAttr(b.iconPath)}">`,
1295
+ // Both the standards-based and the legacy Apple capable meta: Chromium
1296
+ // deprecates `apple-mobile-web-app-capable` in favour of
1297
+ // `mobile-web-app-capable`, while iOS Safari still reads the apple one.
1298
+ '<meta name="mobile-web-app-capable" content="yes">',
1299
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
1300
+ `<meta name="theme-color" content="${escAttr(b.themeColor)}">`,
1301
+ swRegisterScript(s)
1302
+ ].join("\n");
1303
+ }
1304
+ function pwaCensus(d) {
1305
+ const manifestLinked = PWA_SURFACES.filter((s) => {
1306
+ const html = d.shellHtml(s.shellFile);
1307
+ return html != null && html.includes(`"${s.manifestPath}"`);
1308
+ }).length;
1309
+ const iconsPresent = d.iconExists() ? PWA_SURFACES.length : 0;
1310
+ return { surfaces: PWA_SURFACES.length, manifestLinked, iconsPresent };
1311
+ }
1312
+
1230
1313
  // server/webchat-descriptor.ts
1231
1314
  import { resolve, join as join2 } from "path";
1232
1315
  function webchatGatewayUrl() {
@@ -9153,7 +9236,7 @@ app9.post("/", async (c) => {
9153
9236
  const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
9154
9237
  return `${k}=${safe}`;
9155
9238
  }).join(" ");
9156
- const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click"]);
9239
+ const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa"]);
9157
9240
  if (TAGGED_PREFIX_SOURCES.has(source)) {
9158
9241
  console.log(
9159
9242
  `[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
@@ -11804,6 +11887,32 @@ function sortDirEntries(entries) {
11804
11887
  return aKey.localeCompare(bKey);
11805
11888
  });
11806
11889
  }
11890
+ var PROTECTED_DELETE_SEGMENTS = {
11891
+ ".git": "git",
11892
+ ".claude": "claude",
11893
+ secrets: "secrets",
11894
+ agents: "agents",
11895
+ specialists: "specialists",
11896
+ state: "state"
11897
+ };
11898
+ var PROTECTED_DELETE_BASENAMES = {
11899
+ "account.json": "account-json",
11900
+ "wa-channel-bindings.json": "channel-bindings",
11901
+ "webchat-channel-bindings.json": "channel-bindings"
11902
+ };
11903
+ function isProtectedFromDeletion(relPath) {
11904
+ const segments = relPath.split("/").filter(Boolean);
11905
+ for (const seg of segments) {
11906
+ if (Object.hasOwn(PROTECTED_DELETE_SEGMENTS, seg)) {
11907
+ return { protected: true, rule: PROTECTED_DELETE_SEGMENTS[seg] };
11908
+ }
11909
+ }
11910
+ const base = segments[segments.length - 1];
11911
+ if (base && Object.hasOwn(PROTECTED_DELETE_BASENAMES, base)) {
11912
+ return { protected: true, rule: PROTECTED_DELETE_BASENAMES[base] };
11913
+ }
11914
+ return { protected: false };
11915
+ }
11807
11916
  async function knowledgeDocOwnedByAccount(accountId, attachmentId) {
11808
11917
  if (!attachmentId) return false;
11809
11918
  const session = getSession();
@@ -12035,9 +12144,9 @@ app19.delete("/", requireAdminSession, async (c) => {
12035
12144
  return c.json({ error: "Not found" }, 404);
12036
12145
  }
12037
12146
  const base = basename6(absolute);
12038
- const segments = relPath.split("/").filter(Boolean);
12039
- if (base === "account.json" || segments.includes(".git")) {
12040
- console.error(`[data] file-delete blocked path="${relPath}" reason="protected"`);
12147
+ const protection = isProtectedFromDeletion(relPath);
12148
+ if (protection.protected) {
12149
+ console.error(`[data] file-delete blocked path="${relPath}" reason="protected" rule="${protection.rule}"`);
12041
12150
  return c.json({ error: "Protected file \u2014 refusing to delete" }, 403);
12042
12151
  }
12043
12152
  try {
@@ -18188,10 +18297,11 @@ app49.post("/api/remote-auth/set-password", async (c) => {
18188
18297
  });
18189
18298
  app49.route("/api/_client-error", client_error_default);
18190
18299
  console.log("[client-error-route] mounted");
18300
+ var PWA_PUBLIC_PATHS = /* @__PURE__ */ new Set(["/sw.js", ...PWA_SURFACES.map((s) => s.manifestPath)]);
18191
18301
  app49.use("*", async (c, next) => {
18192
18302
  const host = (c.req.header("host") ?? "").split(":")[0];
18193
18303
  const path2 = c.req.path;
18194
- if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/")) {
18304
+ if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/") || PWA_PUBLIC_PATHS.has(path2)) {
18195
18305
  await next();
18196
18306
  return;
18197
18307
  }
@@ -18338,6 +18448,17 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
18338
18448
  icon: brandIconPath,
18339
18449
  logoContainsName: !!BRAND.logoContainsName
18340
18450
  })}</script>`;
18451
+ var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
18452
+ var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
18453
+ var brandIconFsPath = resolve29(process.cwd(), "public", brandIconPath.replace(/^\//, ""));
18454
+ var brandIconExists = existsSync27(brandIconFsPath);
18455
+ var SW_SOURCE = (() => {
18456
+ try {
18457
+ return readFileSync26(resolve29(process.cwd(), "public", "sw.js"), "utf-8");
18458
+ } catch {
18459
+ return null;
18460
+ }
18461
+ })();
18341
18462
  function readInstalledVersion() {
18342
18463
  try {
18343
18464
  if (!PLATFORM_ROOT9) return "unknown";
@@ -18388,11 +18509,13 @@ function cachedHtml(file) {
18388
18509
  const productNameEsc = escapeHtml(BRAND.productName);
18389
18510
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
18390
18511
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
18391
- const headInjection = file === "index.html" ? `${brandScript}
18512
+ const pwaSurface = pwaSurfaceForShell(file);
18513
+ const pwaHead = pwaSurface ? "\n" + pwaHeadTags(pwaSurface, { iconPath: brandIconPath, themeColor: brandThemeColor }) + "\n" : "";
18514
+ const headInjection = (file === "index.html" ? `${brandScript}
18392
18515
  ${versionScript}
18393
18516
  ${clientErrorReporterScript}
18394
18517
  ` : `${brandScript}
18395
- `;
18518
+ `) + pwaHead;
18396
18519
  html = html.replace("</head>", `${headInjection}</head>`);
18397
18520
  htmlCache.set(file, html);
18398
18521
  }
@@ -18534,6 +18657,30 @@ app49.post("/api/vnc/client-event", async (c) => {
18534
18657
  app49.get("/g/:slug", (c) => {
18535
18658
  return c.html(brandedPublicHtml());
18536
18659
  });
18660
+ for (const pwa of PWA_SURFACES) {
18661
+ app49.get(pwa.manifestPath, (c) => {
18662
+ const manifest = buildManifest(pwa, {
18663
+ productName: BRAND.productName,
18664
+ iconPath: brandIconPath,
18665
+ themeColor: brandThemeColor,
18666
+ backgroundColor: brandBackgroundColor
18667
+ });
18668
+ c.header("Content-Type", MANIFEST_CONTENT_TYPE);
18669
+ console.log(
18670
+ `[pwa] op=manifest surface=${pwa.surface} brand=${BRAND.productName} status=200 ct=${MANIFEST_CONTENT_TYPE} icon=${brandIconPath} iconExists=${brandIconExists}`
18671
+ );
18672
+ return c.body(JSON.stringify(manifest));
18673
+ });
18674
+ }
18675
+ app49.get("/sw.js", (c) => {
18676
+ if (SW_SOURCE == null) {
18677
+ console.error("[pwa] op=sw status=500 reason=sw-source-missing");
18678
+ return c.text("Service worker unavailable", 500);
18679
+ }
18680
+ c.header("Content-Type", SW_CONTENT_TYPE);
18681
+ console.log(`[pwa] op=sw status=200 ct=${SW_CONTENT_TYPE}`);
18682
+ return c.body(SW_SOURCE);
18683
+ });
18537
18684
  app49.get("/graph", (c) => {
18538
18685
  const host = (c.req.header("host") ?? "").split(":")[0];
18539
18686
  if (isPublicHost(host) || isOperatorHost(host, getOperatorDomains())) return c.text("Not found", 404);
@@ -18591,6 +18738,21 @@ var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
18591
18738
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
18592
18739
  var httpServer = serve({ fetch: app49.fetch, port, hostname });
18593
18740
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
18741
+ {
18742
+ const census = pwaCensus({
18743
+ shellHtml: (file) => {
18744
+ try {
18745
+ return cachedHtml(file);
18746
+ } catch {
18747
+ return null;
18748
+ }
18749
+ },
18750
+ iconExists: () => brandIconExists
18751
+ });
18752
+ console.log(
18753
+ `[pwa-census] surfaces=${census.surfaces} manifest-linked=${census.manifestLinked} icons-present=${census.iconsPresent}`
18754
+ );
18755
+ }
18594
18756
  startAuthHealthHeartbeat();
18595
18757
  {
18596
18758
  const loopHist = monitorEventLoopDelay({ resolution: 50 });