attachpage 0.1.0 → 0.1.2

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 JP Technology Services
3
+ Copyright (c) 2026 JP Technology Services LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Send a website like an attachment. Point it at a folder with an `index.html`; get a link.
4
4
 
5
- AttachPage is in an invite-only private beta. Sign in once with an invited account:
5
+ Sign in once with your Google or Microsoft account:
6
6
 
7
7
  ```bash
8
+ npx attachpage connect # add AttachPage to Claude Code, Codex, Cursor, VS Code, Gemini CLI
8
9
  npx attachpage login # approve in your browser, once
9
10
  npx attachpage ./dist # publish (private by default)
10
11
  npx attachpage send ./dist --to client@example.com --message "Here's the new version"
@@ -15,8 +16,8 @@ npx attachpage ./dist --site <pageId> --notify "Applied your feedback"
15
16
  Pages are private by default: only the people you send them to can open them, each with a
16
17
  personal link you can revoke. Recipients can comment on the page and approve it; `feedback`
17
18
  lists what they said with the element they pointed at, which is handy for agents. Add `--public`
18
- to make a page open to anyone with the link. Publishing without an account (public pages that
19
- expire after 48 hours) is paused during the beta.
19
+ to make a page open to anyone with the link. Without signing in, `npx attachpage ./dist` publishes a
20
+ public page that expires after 48 hours and prints a link to keep it.
20
21
 
21
22
  - No dependencies, Node 18+.
22
23
  - Your files are uploaded exactly as they are. Nothing is built, bundled or rewritten.
@@ -48,3 +49,12 @@ Setup for Claude, ChatGPT, Cursor, VS Code and Codex: https://attachpage.com/age
48
49
  Agent instructions: https://attachpage.com/skill.md.
49
50
 
50
51
  Support: support@attachpage.com. MIT licensed, by JP Technology Services.
52
+
53
+ ## Connect your AI apps
54
+
55
+ `npx attachpage connect` adds the AttachPage MCP server (`https://attachpage.com/mcp`) to the AI apps
56
+ it finds: Claude Code, OpenAI Codex and Gemini CLI through their own commands, and Cursor and VS Code
57
+ through their user config files (backed up first; existing entries and files it cannot parse are left
58
+ alone). `--dry-run` shows what it would change; `--client cursor,vscode` limits it to some apps. Each
59
+ app asks you to sign in to AttachPage the first time you use it.
60
+
@@ -2,10 +2,11 @@
2
2
  /**
3
3
  * AttachPage command line: publish a folder, send it to people, read their feedback.
4
4
  *
5
- * npx attachpage ./dist publish as you (anonymous 48 h pages are paused in the beta)
5
+ * npx attachpage ./dist publish (signed in: private; signed out: public for 48 h)
6
6
  * npx attachpage send ./dist --to a@b.com publish privately and email personal links
7
7
  * npx attachpage login sign in from the terminal (approve in your browser)
8
8
  * npx attachpage feedback <siteId> what recipients said (open items)
9
+ * npx attachpage connect add AttachPage to the AI apps on this computer
9
10
  *
10
11
  * No dependencies. Credentials: ATTACHPAGE_API_KEY, or the key `login` saved in the operating system's
11
12
  * password store (or in ~/.attachpage/credentials with `login --store file`).
@@ -15,7 +16,7 @@ import { chmod, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/pr
15
16
  import { homedir } from "node:os";
16
17
  import { dirname, join, relative, resolve, sep } from "node:path";
17
18
 
18
- const VERSION = "0.1.0";
19
+ const VERSION = "0.1.1";
19
20
  const CREDENTIALS = process.env.ATTACHPAGE_CREDENTIALS_FILE || join(homedir(), ".attachpage", "credentials");
20
21
  const SKIP_DIRS = new Set([".git", "node_modules", ".svn", ".hg"]);
21
22
  const SKIP_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
@@ -404,9 +405,7 @@ async function publish({ send = false } = {}) {
404
405
  const anon = await api(origin, "POST", "/api/v1/anonymous/publish", {
405
406
  body: { siteName: name, slug, files: manifest },
406
407
  }).catch((e) =>
407
- fail(
408
- e.status === 403 ? `${e.message}\nSign in with an invited account: npx attachpage login` : e.message,
409
- ),
408
+ fail(e.status === 403 ? `${e.message}\nSign in to publish: npx attachpage login` : e.message),
410
409
  );
411
410
  await uploadAll(anon.targets, files, anon.uploadToken, origin);
412
411
  const fin = await api(origin, "POST", anon.next, { key: anon.uploadToken, body: {} });
@@ -562,17 +561,186 @@ async function whoami() {
562
561
  });
563
562
  }
564
563
 
564
+ // ---------- connect ----------
565
+ //
566
+ // Add the AttachPage MCP server to the AI apps on this computer. Apps with their own command line
567
+ // (Claude Code, Codex, Gemini CLI) are set up through it; Cursor and VS Code through their user
568
+ // config file, backed up first. Existing AttachPage entries are left alone, and a file that cannot be
569
+ // read as plain JSON is never rewritten (the snippet to paste is printed instead).
570
+
571
+ const CONNECT_HOME = process.env.ATTACHPAGE_CONNECT_HOME || homedir();
572
+
573
+ /** Run an app's own CLI. Windows npm shims are .cmd files, which need a shell; every argument is a
574
+ * fixed token or the validated server URL, so nothing needs quoting. */
575
+ function runCli(bin, args) {
576
+ return new Promise((done) => {
577
+ let child;
578
+ try {
579
+ child = spawn(bin, args, {
580
+ stdio: ["ignore", "pipe", "pipe"],
581
+ shell: process.platform === "win32",
582
+ windowsHide: true,
583
+ });
584
+ } catch {
585
+ return done({ code: -1, missing: true, out: "" });
586
+ }
587
+ let out = "";
588
+ child.stdout.on("data", (d) => {
589
+ out += d;
590
+ });
591
+ child.stderr.on("data", (d) => {
592
+ out += d;
593
+ });
594
+ child.on("error", () => done({ code: -1, missing: true, out }));
595
+ child.on("close", (code) =>
596
+ done({ code: code ?? -1, missing: code === 9009 || /not recognized|not found/i.test(out), out }),
597
+ );
598
+ });
599
+ }
600
+
601
+ async function readJsonFile(file) {
602
+ try {
603
+ return { ok: true, data: JSON.parse(await readFile(file, "utf8")) };
604
+ } catch (e) {
605
+ return e.code === "ENOENT" ? { ok: true, data: {} } : { ok: false };
606
+ }
607
+ }
608
+
609
+ async function dirExists(dir) {
610
+ try {
611
+ return (await stat(dir)).isDirectory();
612
+ } catch {
613
+ return false;
614
+ }
615
+ }
616
+
617
+ /** Add `entry` under `root[key].attachpage` in a JSON config file. */
618
+ async function addToJsonConfig(file, key, entry, dryRun) {
619
+ const read = await readJsonFile(file);
620
+ if (!read.ok)
621
+ return {
622
+ status: "manual",
623
+ note: `could not read ${file} as plain JSON; add this yourself:\n${JSON.stringify({ [key]: { attachpage: entry } }, null, 2)}`,
624
+ };
625
+ const data = read.data && typeof read.data === "object" ? read.data : {};
626
+ if (data[key]?.attachpage) return { status: "already" };
627
+ if (dryRun) return { status: "would-add", note: file };
628
+ try {
629
+ const existing = await readFile(file, "utf8");
630
+ await writeFile(`${file}.bak-attachpage`, existing, { flag: "wx" }).catch(() => {});
631
+ } catch {
632
+ /* new file */
633
+ }
634
+ data[key] = { ...(data[key] ?? {}), attachpage: entry };
635
+ await mkdir(dirname(file), { recursive: true });
636
+ await writeFile(file, `${JSON.stringify(data, null, 2)}\n`);
637
+ return { status: "added", note: file };
638
+ }
639
+
640
+ function vscodeUserDir() {
641
+ if (process.platform === "win32")
642
+ return join(process.env.APPDATA || join(CONNECT_HOME, "AppData", "Roaming"), "Code", "User");
643
+ if (process.platform === "darwin")
644
+ return join(CONNECT_HOME, "Library", "Application Support", "Code", "User");
645
+ return join(CONNECT_HOME, ".config", "Code", "User");
646
+ }
647
+
648
+ async function connect() {
649
+ const { origin } = await readCredentials();
650
+ if (!SAFE.test(origin)) fail("That --origin cannot be used here.");
651
+ const url = `${origin}/mcp`;
652
+ const dryRun = flags.has("dry-run");
653
+ const only = typeof flags.get("client") === "string" ? String(flags.get("client")).split(",") : null;
654
+ const viaCli = [
655
+ {
656
+ id: "claude-code",
657
+ name: "Claude Code",
658
+ bin: "claude",
659
+ args: ["mcp", "add", "--transport", "http", "--scope", "user", "attachpage", url],
660
+ next: "In Claude Code, run /mcp and sign in to AttachPage.",
661
+ },
662
+ {
663
+ id: "codex",
664
+ name: "OpenAI Codex",
665
+ bin: "codex",
666
+ args: ["mcp", "add", "attachpage", "--url", url],
667
+ next: "Run: codex mcp login attachpage",
668
+ },
669
+ {
670
+ id: "gemini",
671
+ name: "Gemini CLI",
672
+ bin: "gemini",
673
+ args: ["mcp", "add", "--transport", "http", "attachpage", url],
674
+ next: "In Gemini CLI, run /mcp auth attachpage.",
675
+ },
676
+ ];
677
+ const results = [];
678
+ for (const c of viaCli) {
679
+ if (only && !only.includes(c.id)) continue;
680
+ const probe = await runCli(c.bin, ["--version"]);
681
+ if (probe.missing || probe.code !== 0) {
682
+ results.push({ app: c.name, status: "not-found" });
683
+ continue;
684
+ }
685
+ if (dryRun) {
686
+ results.push({ app: c.name, status: "would-add", note: `${c.bin} ${c.args.join(" ")}` });
687
+ continue;
688
+ }
689
+ const r = await runCli(c.bin, c.args);
690
+ if (r.code === 0) results.push({ app: c.name, status: "added", note: c.next });
691
+ else if (/already exists|already configured|already added/i.test(r.out))
692
+ results.push({ app: c.name, status: "already" });
693
+ else results.push({ app: c.name, status: "failed", note: r.out.trim().split("\n").slice(-1)[0] });
694
+ }
695
+ if (!only || only.includes("cursor")) {
696
+ const dir = join(CONNECT_HOME, ".cursor");
697
+ results.push(
698
+ (await dirExists(dir))
699
+ ? { app: "Cursor", ...(await addToJsonConfig(join(dir, "mcp.json"), "mcpServers", { url }, dryRun)) }
700
+ : { app: "Cursor", status: "not-found" },
701
+ );
702
+ }
703
+ if (!only || only.includes("vscode")) {
704
+ const dir = vscodeUserDir();
705
+ results.push(
706
+ (await dirExists(dir))
707
+ ? {
708
+ app: "VS Code",
709
+ ...(await addToJsonConfig(join(dir, "mcp.json"), "servers", { type: "http", url }, dryRun)),
710
+ }
711
+ : { app: "VS Code", status: "not-found" },
712
+ );
713
+ }
714
+ const label = {
715
+ added: "added",
716
+ already: "already set up",
717
+ "would-add": "would add",
718
+ "not-found": "not installed",
719
+ manual: "needs a manual step",
720
+ failed: "failed",
721
+ };
722
+ const lines = results.map((r) => ` ${r.app.padEnd(14)} ${label[r.status]}${r.note ? `: ${r.note}` : ""}`);
723
+ lines.push(
724
+ "",
725
+ `Server: ${url}`,
726
+ "Each app asks you to sign in to AttachPage the first time you use it.",
727
+ "Claude and ChatGPT on the web: Settings > Connectors > Add custom connector, with the server above.",
728
+ );
729
+ out(lines.join("\n"), { server: url, dryRun, results });
730
+ }
731
+
565
732
  const HELP = `attachpage ${VERSION}: send websites like attachments
566
733
 
567
734
  Usage
568
735
  attachpage <folder> [--name "Q3 report"] [--slug q3-report] [--public] [--site <id> [--notify "note"]]
569
736
  attachpage send <folder> --to a@b.com,c@d.com [--message "..."] [--expires 7]
570
737
  attachpage login [--no-browser] [--store file] | logout | whoami
738
+ attachpage connect [--client claude-code,codex,gemini,cursor,vscode] [--dry-run]
571
739
  attachpage feedback <siteId> [--all]
572
740
 
573
- AttachPage is an invite-only private beta: run \`attachpage login\` with an invited account.
574
- Pages are private by default: only the people you send them to can open them.
575
- (Publishing without an account, as public 48-hour pages, is paused during the beta.)
741
+ Sign in once with \`attachpage login\` (Google or Microsoft account). Signed-in pages are private by
742
+ default: only the people you send them to can open them. Without signing in, a folder is published
743
+ as a public page that expires in 48 hours, with a link to keep it.
576
744
  Options: --json for machine-readable output, --origin to use another server.
577
745
  \`login\` keeps the key in your system's password store (Windows Credential Manager, macOS Keychain,
578
746
  or the Linux keyring); --store file keeps it in ~/.attachpage/credentials instead.`;
@@ -584,6 +752,7 @@ try {
584
752
  else if (command === "login") await login();
585
753
  else if (command === "logout") await logout();
586
754
  else if (command === "whoami") await whoami();
755
+ else if (command === "connect") await connect();
587
756
  else if (command === "feedback") await feedback();
588
757
  else if (command === "send") await publish({ send: true });
589
758
  else await publish();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "attachpage",
3
- "version": "0.1.0",
4
- "description": "Send websites like attachments: publish a folder, email personal links, read feedback. Command line for AttachPage (invite-only private beta).",
3
+ "version": "0.1.2",
4
+ "description": "Send websites like attachments: publish a folder, email personal links, read feedback. Command line for AttachPage.",
5
5
  "keywords": [
6
6
  "publish",
7
7
  "static-site",
@@ -12,11 +12,16 @@
12
12
  "attachpage"
13
13
  ],
14
14
  "homepage": "https://attachpage.com",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/colossalbreacker/attachpage-skill.git",
18
+ "directory": "cli"
19
+ },
15
20
  "bugs": {
16
21
  "email": "support@attachpage.com"
17
22
  },
18
23
  "license": "MIT",
19
- "author": "JP Technology Services (https://jptechnologyservices.com)",
24
+ "author": "JP Technology Services LLC (https://jptechnologyservices.com)",
20
25
  "type": "module",
21
26
  "bin": {
22
27
  "attachpage": "bin/attachpage.mjs"
@@ -25,7 +30,7 @@
25
30
  "bin/attachpage.mjs"
26
31
  ],
27
32
  "scripts": {
28
- "test": "node --test test/credentials.test.mjs"
33
+ "test": "node --test test/credentials.test.mjs test/connect.test.mjs"
29
34
  },
30
35
  "engines": {
31
36
  "node": ">=18"