peekable 0.1.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/dist/api.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type ShareConfig } from "./config.js";
2
+ export declare function api(config: ShareConfig, method: string, path: string, body?: unknown): Promise<any>;
3
+ export declare function apiNoAuth(url: string, method: string, path: string, body?: unknown): Promise<any>;
package/dist/api.js ADDED
@@ -0,0 +1,27 @@
1
+ export async function api(config, method, path, body) {
2
+ const res = await fetch(`${config.url}/api${path}`, {
3
+ method,
4
+ headers: {
5
+ Authorization: `Bearer ${config.api_key}`,
6
+ "Content-Type": "application/json",
7
+ },
8
+ body: body ? JSON.stringify(body) : undefined,
9
+ });
10
+ if (!res.ok) {
11
+ const err = await res.json().catch(() => ({ error: res.statusText }));
12
+ throw new Error(err.error ?? res.statusText);
13
+ }
14
+ return res.json();
15
+ }
16
+ export async function apiNoAuth(url, method, path, body) {
17
+ const res = await fetch(`${url}/api${path}`, {
18
+ method,
19
+ headers: { "Content-Type": "application/json" },
20
+ body: body ? JSON.stringify(body) : undefined,
21
+ });
22
+ if (!res.ok) {
23
+ const err = await res.json().catch(() => ({ error: res.statusText }));
24
+ throw new Error(err.error ?? res.statusText);
25
+ }
26
+ return res.json();
27
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const closeCommand: Command;
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import { requireConfig } from "../config.js";
3
+ import { api } from "../api.js";
4
+ export const closeCommand = new Command("close")
5
+ .argument("<session-id>", "Session ID")
6
+ .description("Close a session")
7
+ .option("--json", "Output JSON")
8
+ .action(async (sessionId, opts) => {
9
+ const config = requireConfig();
10
+ await api(config, "DELETE", `/sessions/${sessionId}`);
11
+ if (opts.json) {
12
+ console.log(JSON.stringify({ ok: true }));
13
+ }
14
+ else {
15
+ console.log(`Closed session ${sessionId}`);
16
+ }
17
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const createCommand: Command;
@@ -0,0 +1,18 @@
1
+ import { Command } from "commander";
2
+ import { requireConfig } from "../config.js";
3
+ import { api } from "../api.js";
4
+ export const createCommand = new Command("create")
5
+ .argument("<name>", "Session name")
6
+ .description("Create a new sharing session")
7
+ .option("--json", "Output JSON")
8
+ .action(async (name, opts) => {
9
+ const config = requireConfig();
10
+ const data = await api(config, "POST", "/sessions", { name });
11
+ if (opts.json) {
12
+ console.log(JSON.stringify(data));
13
+ }
14
+ else {
15
+ console.log(`Created session: ${data.id}`);
16
+ console.log(`URL: ${data.url}`);
17
+ }
18
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const feedbackCommand: Command;
@@ -0,0 +1,26 @@
1
+ import { Command } from "commander";
2
+ import { requireConfig } from "../config.js";
3
+ import { api } from "../api.js";
4
+ export const feedbackCommand = new Command("feedback")
5
+ .argument("<session-id>", "Session ID")
6
+ .description("Fetch feedback for a session")
7
+ .option("--json", "Output JSON")
8
+ .action(async (sessionId, opts) => {
9
+ const config = requireConfig();
10
+ const data = await api(config, "GET", `/sessions/${sessionId}/feedback`);
11
+ if (opts.json) {
12
+ console.log(JSON.stringify(data));
13
+ return;
14
+ }
15
+ if (data.feedback.length === 0) {
16
+ console.log("No feedback yet.");
17
+ return;
18
+ }
19
+ for (const group of data.feedback) {
20
+ console.log(`\n--- Version ${group.version} ---`);
21
+ for (const event of group.events) {
22
+ const who = event.viewer ?? "Anonymous";
23
+ console.log(` ${who} chose "${event.choice}" — ${event.label}`);
24
+ }
25
+ }
26
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const initCommand: Command;
@@ -0,0 +1,82 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, mkdirSync, cpSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { homedir } from "os";
5
+ import { fileURLToPath } from "url";
6
+ import { requireConfig } from "../config.js";
7
+ import { api } from "../api.js";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ export const initCommand = new Command("init")
10
+ .description("Set up peekable — install Claude Code skill and verify connection")
11
+ .option("--json", "Output JSON")
12
+ .action(async (opts) => {
13
+ const config = requireConfig();
14
+ const result = { status: "ok" };
15
+ // 1. Test connection
16
+ try {
17
+ const res = await fetch(`${config.url}/health`);
18
+ if (!res.ok)
19
+ throw new Error("Health check failed");
20
+ }
21
+ catch {
22
+ const msg = `Cannot reach server at ${config.url}`;
23
+ if (opts.json) {
24
+ console.log(JSON.stringify({ status: "error", error: msg }));
25
+ }
26
+ else {
27
+ console.error(msg);
28
+ }
29
+ process.exit(1);
30
+ }
31
+ if (!opts.json)
32
+ console.log("Connection verified.");
33
+ // 2. Detect Claude Code and install skill
34
+ const claudeDir = join(homedir(), ".claude");
35
+ const claudeCodeDetected = existsSync(claudeDir);
36
+ result.claude_code = claudeCodeDetected;
37
+ if (claudeCodeDetected) {
38
+ const skillDir = join(claudeDir, "skills", "peekable");
39
+ // Resolve skill source relative to compiled output location
40
+ const skillSource = join(__dirname, "..", "..", "skill", "SKILL.md");
41
+ if (existsSync(skillSource)) {
42
+ mkdirSync(skillDir, { recursive: true });
43
+ cpSync(skillSource, join(skillDir, "SKILL.md"));
44
+ result.skill_installed = true;
45
+ if (!opts.json)
46
+ console.log("Claude Code /peekable skill installed.");
47
+ }
48
+ else {
49
+ result.skill_installed = false;
50
+ if (!opts.json)
51
+ console.log("Claude Code detected but skill template not found.");
52
+ }
53
+ }
54
+ else {
55
+ result.skill_installed = false;
56
+ if (!opts.json)
57
+ console.log("Claude Code not detected — skipping skill install.");
58
+ }
59
+ // 3. Test push
60
+ try {
61
+ const session = await api(config, "POST", "/sessions", { name: "__share_init_test__" });
62
+ await api(config, "POST", `/sessions/${session.id}/push`, {
63
+ html: "<html><body><p>Share init test</p></body></html>",
64
+ });
65
+ await api(config, "DELETE", `/sessions/${session.id}`);
66
+ result.test_push = "passed";
67
+ if (!opts.json)
68
+ console.log("Test push verified.");
69
+ }
70
+ catch (err) {
71
+ result.test_push = "failed";
72
+ result.test_error = err.message;
73
+ if (!opts.json)
74
+ console.error(`Test push failed: ${err.message}`);
75
+ }
76
+ if (opts.json) {
77
+ console.log(JSON.stringify(result));
78
+ }
79
+ else {
80
+ console.log("\nReady to go! Try: peekable create \"My First Session\"");
81
+ }
82
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const listCommand: Command;
@@ -0,0 +1,22 @@
1
+ import { Command } from "commander";
2
+ import { requireConfig } from "../config.js";
3
+ import { api } from "../api.js";
4
+ export const listCommand = new Command("list")
5
+ .description("List active sessions")
6
+ .option("--json", "Output JSON")
7
+ .action(async (opts) => {
8
+ const config = requireConfig();
9
+ const data = await api(config, "GET", "/sessions");
10
+ if (opts.json) {
11
+ console.log(JSON.stringify(data));
12
+ return;
13
+ }
14
+ if (data.sessions.length === 0) {
15
+ console.log("No active sessions.");
16
+ return;
17
+ }
18
+ console.log("Active sessions:");
19
+ for (const s of data.sessions) {
20
+ console.log(` ${s.id} ${s.name} (${new Date(s.created_at).toLocaleDateString()})`);
21
+ }
22
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const pushCommand: Command;
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "fs";
3
+ import { requireConfig } from "../config.js";
4
+ import { api } from "../api.js";
5
+ export const pushCommand = new Command("push")
6
+ .argument("<session-id>", "Session ID")
7
+ .argument("<file>", "HTML file path")
8
+ .description("Push an HTML file as a new version")
9
+ .option("--json", "Output JSON")
10
+ .action(async (sessionId, file, opts) => {
11
+ const config = requireConfig();
12
+ const html = readFileSync(file, "utf-8");
13
+ const data = await api(config, "POST", `/sessions/${sessionId}/push`, { html });
14
+ if (opts.json) {
15
+ console.log(JSON.stringify(data));
16
+ }
17
+ else {
18
+ console.log(`Pushed v${data.version} to ${sessionId}`);
19
+ }
20
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const registerCommand: Command;
@@ -0,0 +1,51 @@
1
+ import { Command } from "commander";
2
+ import { writeConfig, readConfig } from "../config.js";
3
+ import { apiNoAuth } from "../api.js";
4
+ const DEFAULT_URL = "https://peekable.fyi";
5
+ export const registerCommand = new Command("register")
6
+ .description("Register for an API key")
7
+ .requiredOption("--name <name>", "Your display name")
8
+ .requiredOption("--email <email>", "Your email address")
9
+ .option("--url <url>", "Server URL", DEFAULT_URL)
10
+ .option("--json", "Output JSON")
11
+ .action(async (opts) => {
12
+ const existing = readConfig();
13
+ if (existing) {
14
+ if (opts.json) {
15
+ console.log(JSON.stringify({ error: "Already registered. Config exists at ~/.peekable/config.json" }));
16
+ }
17
+ else {
18
+ console.error("Already registered. Config exists at ~/.peekable/config.json");
19
+ console.error("Delete ~/.peekable/config.json to re-register.");
20
+ }
21
+ process.exit(1);
22
+ }
23
+ try {
24
+ const data = await apiNoAuth(opts.url, "POST", "/register", {
25
+ name: opts.name,
26
+ email: opts.email,
27
+ });
28
+ writeConfig({
29
+ url: opts.url,
30
+ api_key: data.api_key,
31
+ name: opts.name,
32
+ });
33
+ if (opts.json) {
34
+ console.log(JSON.stringify({ api_key: data.api_key, url: opts.url, name: opts.name }));
35
+ }
36
+ else {
37
+ console.log(`Registered as ${opts.name}.`);
38
+ console.log(`API key saved to ~/.peekable/config.json`);
39
+ console.log(`\nNext step: run \`peekable init\` to complete setup.`);
40
+ }
41
+ }
42
+ catch (err) {
43
+ if (opts.json) {
44
+ console.log(JSON.stringify({ error: err.message }));
45
+ }
46
+ else {
47
+ console.error(`Registration failed: ${err.message}`);
48
+ }
49
+ process.exit(1);
50
+ }
51
+ });
@@ -0,0 +1,9 @@
1
+ export interface ShareConfig {
2
+ url: string;
3
+ api_key: string;
4
+ name: string;
5
+ }
6
+ export declare function readConfig(): ShareConfig | null;
7
+ export declare function writeConfig(config: ShareConfig): void;
8
+ export declare function requireConfig(): ShareConfig;
9
+ export declare function getConfigPath(): string;
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ const CONFIG_DIR = join(homedir(), ".peekable");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export function readConfig() {
7
+ if (process.env.SHARE_URL && process.env.SHARE_API_KEY) {
8
+ return {
9
+ url: process.env.SHARE_URL,
10
+ api_key: process.env.SHARE_API_KEY,
11
+ name: process.env.SHARE_NAME ?? "User",
12
+ };
13
+ }
14
+ if (!existsSync(CONFIG_FILE))
15
+ return null;
16
+ try {
17
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
18
+ return JSON.parse(raw);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function writeConfig(config) {
25
+ mkdirSync(CONFIG_DIR, { recursive: true });
26
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
27
+ }
28
+ export function requireConfig() {
29
+ const config = readConfig();
30
+ if (!config) {
31
+ console.error("Not configured. Run `peekable register` first.");
32
+ process.exit(1);
33
+ }
34
+ return config;
35
+ }
36
+ export function getConfigPath() {
37
+ return CONFIG_FILE;
38
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { registerCommand } from "./commands/register.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { createCommand } from "./commands/create.js";
6
+ import { pushCommand } from "./commands/push.js";
7
+ import { feedbackCommand } from "./commands/feedback.js";
8
+ import { listCommand } from "./commands/list.js";
9
+ import { closeCommand } from "./commands/close.js";
10
+ program
11
+ .name("peekable")
12
+ .description("Share HTML mockups with collaborators via peekable")
13
+ .version("0.1.0");
14
+ program.addCommand(registerCommand);
15
+ program.addCommand(initCommand);
16
+ program.addCommand(createCommand);
17
+ program.addCommand(pushCommand);
18
+ program.addCommand(feedbackCommand);
19
+ program.addCommand(listCommand);
20
+ program.addCommand(closeCommand);
21
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "peekable",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Share HTML mockups with collaborators — CLI for peekable-server",
6
+ "bin": {
7
+ "peekable": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "commander": "^12"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5",
18
+ "tsx": "^4",
19
+ "@types/node": "^22"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "skill"
24
+ ]
25
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: peekable
3
+ description: Share HTML mockups with collaborators via public URLs. Push from Claude Code, collect structured feedback. Use when the user says "share this", "share with", "/share", "/peekable", "peekable", or wants to get a collaborator's feedback on a mockup.
4
+ ---
5
+
6
+ # Peekable
7
+
8
+ Share HTML mockups and playgrounds with collaborators via public URLs with structured feedback.
9
+
10
+ ## Commands
11
+
12
+ All commands use the globally installed `peekable` CLI. Every command supports `--json` for structured output.
13
+
14
+ ### Share a file
15
+
16
+ When the user wants to share an HTML file (playground, mockup, brainstorming companion screen):
17
+
18
+ 1. Create a session: `peekable create "<name>" --json`
19
+ 2. Push the HTML: `peekable push <session-id> <file-path> --json`
20
+ 3. Return the URL to the user
21
+
22
+ If a session already exists for this topic, reuse it (push creates a new version, collaborators auto-reload).
23
+
24
+ ### Check feedback
25
+
26
+ When the user asks "what did they think?" or "any feedback?":
27
+
28
+ ```bash
29
+ peekable feedback <session-id> --json
30
+ ```
31
+
32
+ Parse the JSON and present conversationally:
33
+ - "Sophia chose Option B (Separate Tools) on v1"
34
+ - "No feedback yet on v2"
35
+
36
+ ### List sessions
37
+
38
+ ```bash
39
+ peekable list --json
40
+ ```
41
+
42
+ ### Close a session
43
+
44
+ ```bash
45
+ peekable close <session-id> --json
46
+ ```
47
+
48
+ ## Behavior
49
+
50
+ - Always use `--json` flag and parse the output for conversation context
51
+ - When sharing, always give the user the full URL so they can send it to their collaborator
52
+ - If the user says "share this" without specifying a file, look for the most recent HTML file in the current brainstorming session directory or the last playground file generated
53
+ - Reuse existing sessions when iterating on the same topic — push creates new versions, collaborators auto-reload via WebSocket