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 +3 -0
- package/dist/api.js +27 -0
- package/dist/commands/close.d.ts +2 -0
- package/dist/commands/close.js +17 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +18 -0
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +26 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +82 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +22 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +20 -0
- package/dist/commands/register.d.ts +2 -0
- package/dist/commands/register.js +51 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/package.json +25 -0
- package/skill/SKILL.md +53 -0
package/dist/api.d.ts
ADDED
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,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,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,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,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,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,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,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
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|