peekable 0.1.1 → 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/README.md +100 -0
- package/dist/api.d.ts +24 -2
- package/dist/api.js +48 -10
- package/dist/command-error.d.ts +3 -0
- package/dist/command-error.js +23 -0
- package/dist/commands/create.js +13 -6
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +141 -0
- package/dist/commands/init.js +3 -3
- package/dist/commands/list.js +13 -2
- package/dist/commands/proxy.js +14 -6
- package/dist/commands/push-url.js +92 -29
- package/dist/commands/push.js +19 -12
- package/dist/commands/register.js +11 -1
- package/dist/commands/uninstall.d.ts +21 -0
- package/dist/commands/uninstall.js +81 -0
- package/dist/config.js +3 -4
- package/dist/index.js +6 -1
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +14 -0
- package/dist/quota.d.ts +1 -0
- package/dist/quota.js +7 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +1 -1
- package/skill/SKILL.md +23 -5
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Peekable CLI
|
|
2
|
+
|
|
3
|
+
Share local HTML mockups, HTML response snapshots, and localhost apps with collaborators.
|
|
4
|
+
Reviewers annotate the page in the browser; you pull structured feedback back into
|
|
5
|
+
your terminal.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g peekable
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## First Run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
peekable register --name "Your Name" --email "you@example.com" --invite-code "your-invite-code"
|
|
17
|
+
peekable init
|
|
18
|
+
peekable create "My first review"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`peekable create` prints a session ID and share URL. Use the session ID with one
|
|
22
|
+
of the publish commands below.
|
|
23
|
+
|
|
24
|
+
## Pick The Right Publish Command
|
|
25
|
+
|
|
26
|
+
Use `push` for a complete standalone HTML file:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
peekable push <session-id> ./out.html
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use `push-url` when a running page returns a mostly self-contained HTML response
|
|
33
|
+
you want to capture:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
peekable push-url <session-id> http://localhost:3000
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `proxy` for a live localhost app with routes, assets, and interactivity:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
peekable proxy 3000 --name "Homepage review"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Pull Feedback
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
peekable feedback <session-id>
|
|
49
|
+
peekable watch <session-id>
|
|
50
|
+
peekable resolve <session-id> <annotation-id>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Free Early Access Limits
|
|
54
|
+
|
|
55
|
+
The hosted free early-access tier allows 3 active sessions per user. Close old
|
|
56
|
+
sessions when you are done:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
peekable list
|
|
60
|
+
peekable close <session-id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Viewer connections are capped on the hosted service so early access stays
|
|
64
|
+
reliable for everyone.
|
|
65
|
+
|
|
66
|
+
`push-url` snapshots localhost by default. To upload HTML fetched from a remote
|
|
67
|
+
URL, pass `--allow-remote --yes`; for private-network URLs, also pass
|
|
68
|
+
`--allow-private`.
|
|
69
|
+
|
|
70
|
+
## Debug Setup
|
|
71
|
+
|
|
72
|
+
If something feels off, run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
peekable doctor
|
|
76
|
+
peekable doctor --json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For a deeper connectivity check that creates, pushes, and closes a temporary
|
|
80
|
+
session:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
peekable doctor --test-push
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The doctor output intentionally avoids HTML payloads, annotation text, and API
|
|
87
|
+
keys so it is safe to paste into a support thread.
|
|
88
|
+
|
|
89
|
+
## Remove Local Setup
|
|
90
|
+
|
|
91
|
+
To remove Peekable from your machine:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
peekable uninstall
|
|
95
|
+
npm uninstall -g peekable
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`peekable uninstall` removes local config at `~/.peekable` and the installed
|
|
99
|
+
Claude Code skill at `~/.claude/skills/peekable`. It does not delete hosted
|
|
100
|
+
sessions or account data.
|
package/dist/api.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
1
|
import { type ShareConfig } from "./config.js";
|
|
2
|
-
export declare
|
|
3
|
-
export declare
|
|
2
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
|
|
3
|
+
export declare class ApiError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
details: Record<string, unknown>;
|
|
6
|
+
constructor(status: number, details: Record<string, unknown>, fallback: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function api(config: ShareConfig, method: string, path: string, body?: unknown, opts?: {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}): Promise<any>;
|
|
11
|
+
export declare function postDebugEvent(config: ShareConfig, body: {
|
|
12
|
+
event_name: string;
|
|
13
|
+
session_id?: string;
|
|
14
|
+
source?: "cli";
|
|
15
|
+
command?: string;
|
|
16
|
+
cli_version?: string;
|
|
17
|
+
status?: string;
|
|
18
|
+
duration_ms?: number;
|
|
19
|
+
error_code?: string;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}): Promise<any>;
|
|
22
|
+
export declare function apiNoAuth(url: string, method: string, path: string, body?: unknown, opts?: {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}): Promise<any>;
|
|
25
|
+
export declare function fetchWithTimeout(input: string | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
package/dist/api.js
CHANGED
|
@@ -1,27 +1,65 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
details;
|
|
5
|
+
constructor(status, details, fallback) {
|
|
6
|
+
super(typeof details.error === "string" ? details.error : fallback);
|
|
7
|
+
this.name = "ApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function api(config, method, path, body, opts = {}) {
|
|
13
|
+
const res = await fetchWithTimeout(`${config.url}/api${path}`, {
|
|
3
14
|
method,
|
|
4
15
|
headers: {
|
|
5
16
|
Authorization: `Bearer ${config.api_key}`,
|
|
6
17
|
"Content-Type": "application/json",
|
|
7
18
|
},
|
|
8
19
|
body: body ? JSON.stringify(body) : undefined,
|
|
9
|
-
});
|
|
20
|
+
}, opts.timeoutMs);
|
|
10
21
|
if (!res.ok) {
|
|
11
|
-
|
|
12
|
-
throw new Error(err.error ?? res.statusText);
|
|
22
|
+
throw await buildApiError(res);
|
|
13
23
|
}
|
|
14
24
|
return res.json();
|
|
15
25
|
}
|
|
16
|
-
export async function
|
|
17
|
-
|
|
26
|
+
export async function postDebugEvent(config, body) {
|
|
27
|
+
return api(config, "POST", "/debug-events", body);
|
|
28
|
+
}
|
|
29
|
+
export async function apiNoAuth(url, method, path, body, opts = {}) {
|
|
30
|
+
const res = await fetchWithTimeout(`${url}/api${path}`, {
|
|
18
31
|
method,
|
|
19
32
|
headers: { "Content-Type": "application/json" },
|
|
20
33
|
body: body ? JSON.stringify(body) : undefined,
|
|
21
|
-
});
|
|
34
|
+
}, opts.timeoutMs);
|
|
22
35
|
if (!res.ok) {
|
|
23
|
-
|
|
24
|
-
throw new Error(err.error ?? res.statusText);
|
|
36
|
+
throw await buildApiError(res);
|
|
25
37
|
}
|
|
26
38
|
return res.json();
|
|
27
39
|
}
|
|
40
|
+
export async function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
return await fetch(input, {
|
|
45
|
+
...init,
|
|
46
|
+
signal: init.signal ?? controller.signal,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err?.name === "AbortError") {
|
|
51
|
+
throw new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function buildApiError(res) {
|
|
60
|
+
const details = await res.json().catch(() => ({ error: res.statusText }));
|
|
61
|
+
const body = details && typeof details === "object" && !Array.isArray(details)
|
|
62
|
+
? details
|
|
63
|
+
: { error: res.statusText };
|
|
64
|
+
return new ApiError(res.status, body, res.statusText);
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiError } from "./api.js";
|
|
2
|
+
export function errorPayload(err) {
|
|
3
|
+
if (err instanceof ApiError) {
|
|
4
|
+
return {
|
|
5
|
+
error: err.message,
|
|
6
|
+
...err.details,
|
|
7
|
+
status: err.status,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return { error: getErrorMessage(err) };
|
|
11
|
+
}
|
|
12
|
+
export function getErrorMessage(err) {
|
|
13
|
+
if (err instanceof Error)
|
|
14
|
+
return err.message;
|
|
15
|
+
return String(err);
|
|
16
|
+
}
|
|
17
|
+
export function printCommandError(prefix, err, json) {
|
|
18
|
+
if (json) {
|
|
19
|
+
console.log(JSON.stringify(errorPayload(err)));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.error(`${prefix}: ${getErrorMessage(err)}`);
|
|
23
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { requireConfig } from "../config.js";
|
|
3
3
|
import { api } from "../api.js";
|
|
4
|
+
import { printCommandError } from "../command-error.js";
|
|
4
5
|
export const createCommand = new Command("create")
|
|
5
6
|
.argument("<name>", "Session name")
|
|
6
7
|
.description("Create a new sharing session")
|
|
7
8
|
.option("--json", "Output JSON")
|
|
8
9
|
.action(async (name, opts) => {
|
|
9
10
|
const config = requireConfig();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
try {
|
|
12
|
+
const data = await api(config, "POST", "/sessions", { name });
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify(data));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(`Created session: ${data.id}`);
|
|
18
|
+
console.log(`URL: ${data.url}`);
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
catch (err) {
|
|
22
|
+
printCommandError("Create failed", err, opts.json);
|
|
23
|
+
process.exit(1);
|
|
17
24
|
}
|
|
18
25
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export type DoctorStatus = "ok" | "failed" | "skipped";
|
|
3
|
+
export interface DoctorReport {
|
|
4
|
+
cliVersion: string;
|
|
5
|
+
nodeVersion: string;
|
|
6
|
+
configPath: string;
|
|
7
|
+
configured: boolean;
|
|
8
|
+
serverUrl: string | null;
|
|
9
|
+
health: DoctorStatus;
|
|
10
|
+
auth: DoctorStatus;
|
|
11
|
+
activeSessions: number | null;
|
|
12
|
+
maxActiveSessions: number | null;
|
|
13
|
+
activeSessionUnlimited?: boolean;
|
|
14
|
+
testPush: DoctorStatus;
|
|
15
|
+
errors?: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const doctorCommand: Command;
|
|
18
|
+
export declare function runDoctor(opts?: {
|
|
19
|
+
testPush?: boolean;
|
|
20
|
+
}): Promise<DoctorReport>;
|
|
21
|
+
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { api, fetchWithTimeout } from "../api.js";
|
|
3
|
+
import { getConfigPath, readConfig } from "../config.js";
|
|
4
|
+
import { getErrorMessage } from "../command-error.js";
|
|
5
|
+
import { formatQuota } from "../quota.js";
|
|
6
|
+
import { CLI_VERSION } from "../version.js";
|
|
7
|
+
export const doctorCommand = new Command("doctor")
|
|
8
|
+
.description("Print a support snapshot for debugging Peekable setup")
|
|
9
|
+
.option("--json", "Output JSON")
|
|
10
|
+
.option("--test-push", "Create, push, and close a temporary test session")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const report = await runDoctor({ testPush: Boolean(opts.testPush) });
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify(report));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.log(formatDoctorReport(report));
|
|
18
|
+
});
|
|
19
|
+
export async function runDoctor(opts = {}) {
|
|
20
|
+
const config = readConfig();
|
|
21
|
+
const report = {
|
|
22
|
+
cliVersion: CLI_VERSION,
|
|
23
|
+
nodeVersion: process.version,
|
|
24
|
+
configPath: redactHomePath(getConfigPath()),
|
|
25
|
+
configured: Boolean(config),
|
|
26
|
+
serverUrl: config?.url ?? null,
|
|
27
|
+
health: "skipped",
|
|
28
|
+
auth: "skipped",
|
|
29
|
+
activeSessions: null,
|
|
30
|
+
maxActiveSessions: null,
|
|
31
|
+
activeSessionUnlimited: false,
|
|
32
|
+
testPush: "skipped",
|
|
33
|
+
errors: [],
|
|
34
|
+
};
|
|
35
|
+
if (!config) {
|
|
36
|
+
report.errors.push("Not configured. Run `peekable register` first.");
|
|
37
|
+
return report;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetchWithTimeout(`${config.url}/health`);
|
|
41
|
+
report.health = res.ok ? "ok" : "failed";
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
report.errors.push(`Health check failed: HTTP ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
report.health = "failed";
|
|
47
|
+
report.errors.push(`Health check failed: ${getErrorMessage(err)}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const data = await getUsageSnapshot(config);
|
|
51
|
+
report.auth = "ok";
|
|
52
|
+
report.activeSessions = data.limits?.activeSessions ?? null;
|
|
53
|
+
report.maxActiveSessions = data.limits?.maxActiveSessions ?? null;
|
|
54
|
+
report.activeSessionUnlimited = Boolean(data.limits?.unlimited);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
report.auth = "failed";
|
|
58
|
+
report.errors.push(`Auth check failed: ${getErrorMessage(err)}`);
|
|
59
|
+
}
|
|
60
|
+
if (opts.testPush && report.auth === "ok") {
|
|
61
|
+
try {
|
|
62
|
+
const session = await api(config, "POST", "/sessions", { name: "__peekable_doctor__" });
|
|
63
|
+
try {
|
|
64
|
+
await api(config, "POST", `/sessions/${session.id}/push`, {
|
|
65
|
+
html: "<html><body><p>Peekable doctor test</p></body></html>",
|
|
66
|
+
});
|
|
67
|
+
report.testPush = "ok";
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await api(config, "DELETE", `/sessions/${session.id}`).catch(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
report.testPush = "failed";
|
|
75
|
+
report.errors.push(`Test push failed: ${getErrorMessage(err)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (report.errors?.length === 0)
|
|
79
|
+
delete report.errors;
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
export function formatDoctorReport(report) {
|
|
83
|
+
const lines = [
|
|
84
|
+
`Peekable CLI: ${report.cliVersion}`,
|
|
85
|
+
`Node: ${report.nodeVersion}`,
|
|
86
|
+
`Config: ${report.configured ? report.configPath : "missing"}`,
|
|
87
|
+
`Server: ${formatServer(report)}`,
|
|
88
|
+
`Auth: ${formatAuth(report.auth)}`,
|
|
89
|
+
`Active sessions: ${formatQuota(report.activeSessions, report.maxActiveSessions, report.activeSessionUnlimited)}`,
|
|
90
|
+
`Test push: ${report.testPush}`,
|
|
91
|
+
];
|
|
92
|
+
if (report.activeSessions !== null &&
|
|
93
|
+
report.maxActiveSessions !== null &&
|
|
94
|
+
!report.activeSessionUnlimited &&
|
|
95
|
+
report.activeSessions >= report.maxActiveSessions) {
|
|
96
|
+
lines.push("Free plan limit reached. Close one with `peekable close <id>`.");
|
|
97
|
+
}
|
|
98
|
+
if (report.errors?.length) {
|
|
99
|
+
lines.push("", "Errors:");
|
|
100
|
+
for (const error of report.errors)
|
|
101
|
+
lines.push(`- ${error}`);
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function formatServer(report) {
|
|
106
|
+
if (!report.serverUrl)
|
|
107
|
+
return "not configured";
|
|
108
|
+
if (report.health === "ok")
|
|
109
|
+
return `${report.serverUrl} reachable`;
|
|
110
|
+
if (report.health === "failed")
|
|
111
|
+
return `${report.serverUrl} unreachable`;
|
|
112
|
+
return `${report.serverUrl} not checked`;
|
|
113
|
+
}
|
|
114
|
+
function formatAuth(status) {
|
|
115
|
+
if (status === "ok")
|
|
116
|
+
return "valid";
|
|
117
|
+
if (status === "failed")
|
|
118
|
+
return "failed";
|
|
119
|
+
return "not checked";
|
|
120
|
+
}
|
|
121
|
+
async function getUsageSnapshot(config) {
|
|
122
|
+
try {
|
|
123
|
+
return await api(config, "GET", "/me/usage");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
const data = await api(config, "GET", "/sessions");
|
|
127
|
+
return {
|
|
128
|
+
limits: {
|
|
129
|
+
activeSessions: data.limits?.activeSessions ?? data.sessions?.length ?? null,
|
|
130
|
+
maxActiveSessions: data.limits?.maxActiveSessions ?? null,
|
|
131
|
+
unlimited: Boolean(data.limits?.unlimited),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function redactHomePath(path) {
|
|
137
|
+
const home = process.env.HOME;
|
|
138
|
+
if (!home || !path.startsWith(home))
|
|
139
|
+
return path;
|
|
140
|
+
return `~${path.slice(home.length)}`;
|
|
141
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { existsSync, mkdirSync, cpSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
4
|
import { fileURLToPath } from "url";
|
|
6
5
|
import { requireConfig } from "../config.js";
|
|
7
6
|
import { api } from "../api.js";
|
|
7
|
+
import { getClaudeDir, getClaudePeekableSkillDir } from "../paths.js";
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
export const initCommand = new Command("init")
|
|
10
10
|
.description("Set up peekable — install Claude Code skill and verify connection")
|
|
@@ -31,11 +31,11 @@ export const initCommand = new Command("init")
|
|
|
31
31
|
if (!opts.json)
|
|
32
32
|
console.log("Connection verified.");
|
|
33
33
|
// 2. Detect Claude Code and install skill
|
|
34
|
-
const claudeDir =
|
|
34
|
+
const claudeDir = getClaudeDir();
|
|
35
35
|
const claudeCodeDetected = existsSync(claudeDir);
|
|
36
36
|
result.claude_code = claudeCodeDetected;
|
|
37
37
|
if (claudeCodeDetected) {
|
|
38
|
-
const skillDir =
|
|
38
|
+
const skillDir = getClaudePeekableSkillDir();
|
|
39
39
|
// Resolve skill source relative to compiled output location
|
|
40
40
|
const skillSource = join(__dirname, "..", "..", "skill", "SKILL.md");
|
|
41
41
|
if (existsSync(skillSource)) {
|
package/dist/commands/list.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { requireConfig } from "../config.js";
|
|
3
3
|
import { api } from "../api.js";
|
|
4
|
+
import { formatQuota } from "../quota.js";
|
|
4
5
|
export const listCommand = new Command("list")
|
|
5
6
|
.description("List active sessions")
|
|
6
7
|
.option("--json", "Output JSON")
|
|
@@ -12,10 +13,20 @@ export const listCommand = new Command("list")
|
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
if (data.sessions.length === 0) {
|
|
15
|
-
|
|
16
|
+
if (data.limits) {
|
|
17
|
+
console.log(`No active sessions. (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)})`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log("No active sessions.");
|
|
21
|
+
}
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
if (data.limits) {
|
|
25
|
+
console.log(`Active sessions (${formatQuota(data.limits.activeSessions, data.limits.maxActiveSessions, data.limits.unlimited)}):`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log("Active sessions:");
|
|
29
|
+
}
|
|
19
30
|
for (const s of data.sessions) {
|
|
20
31
|
console.log(` ${s.id} ${s.name} (${new Date(s.created_at).toLocaleDateString()})`);
|
|
21
32
|
}
|
package/dist/commands/proxy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { watch } from "fs";
|
|
3
3
|
import { requireConfig } from "../config.js";
|
|
4
4
|
import { api } from "../api.js";
|
|
5
|
+
import { printCommandError } from "../command-error.js";
|
|
5
6
|
const MAX_RESPONSE_BYTES = 25 * 1024 * 1024; // 25 MB
|
|
6
7
|
const TEXT_CONTENT_TYPES = [
|
|
7
8
|
"text/",
|
|
@@ -26,7 +27,7 @@ export const proxyCommand = new Command("proxy")
|
|
|
26
27
|
// Validate port
|
|
27
28
|
const port = parseInt(portArg, 10);
|
|
28
29
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
29
|
-
|
|
30
|
+
printCommandError("Proxy failed", new Error("Invalid port: must be 1-65535"), opts.json);
|
|
30
31
|
process.exit(1);
|
|
31
32
|
}
|
|
32
33
|
// Create or reuse session
|
|
@@ -38,11 +39,18 @@ export const proxyCommand = new Command("proxy")
|
|
|
38
39
|
shareUrl = `https://${sessionId}.${baseDomain}`;
|
|
39
40
|
}
|
|
40
41
|
else {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
data = await api(config, "POST", "/sessions", {
|
|
45
|
+
name: opts.name,
|
|
46
|
+
mode: "proxy",
|
|
47
|
+
proxy_target: `localhost:${port}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
printCommandError("Proxy session failed", err, opts.json);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
46
54
|
sessionId = data.id;
|
|
47
55
|
shareUrl = data.url;
|
|
48
56
|
}
|
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { requireConfig } from "../config.js";
|
|
3
|
-
import { api } from "../api.js";
|
|
3
|
+
import { api, fetchWithTimeout } from "../api.js";
|
|
4
|
+
import { printCommandError } from "../command-error.js";
|
|
4
5
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_HTML_BYTES = 5 * 1024 * 1024;
|
|
5
7
|
export const pushUrlCommand = new Command("push-url")
|
|
6
8
|
.argument("<session-id>", "Session ID")
|
|
7
|
-
.argument("<url>", "URL that returns
|
|
8
|
-
.description("Fetch
|
|
9
|
+
.argument("<url>", "URL that returns an HTML response")
|
|
10
|
+
.description("Fetch an HTML response from a URL and push it as a new version")
|
|
9
11
|
.option("--json", "Output JSON")
|
|
12
|
+
.option("--allow-remote", "Allow snapshotting a non-localhost URL")
|
|
13
|
+
.option("--allow-private", "Allow snapshotting a private-network URL")
|
|
14
|
+
.option("-y, --yes", "Confirm the fetched HTML can be uploaded to the session")
|
|
10
15
|
.addHelpText("after", `
|
|
11
|
-
Use this when a running page
|
|
16
|
+
Use this when a running page returns a mostly self-contained HTML response you want to share.
|
|
12
17
|
For standalone HTML files, use: peekable push <session-id> <file>
|
|
13
18
|
For live dev servers with routes/assets/interactivity, use: peekable proxy <port>
|
|
14
19
|
|
|
15
|
-
Note:
|
|
20
|
+
Note: this does not execute JavaScript, use browser cookies, or inline external assets.
|
|
21
|
+
Remote URLs require --allow-remote --yes because the fetched HTML is uploaded to the session.
|
|
16
22
|
`)
|
|
17
23
|
.action(async (sessionId, urlArg, opts) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
html
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
try {
|
|
25
|
+
const config = requireConfig();
|
|
26
|
+
const url = parseHttpUrl(urlArg);
|
|
27
|
+
validateSnapshotTarget(url, opts);
|
|
28
|
+
const html = await fetchHtml(url);
|
|
29
|
+
const data = await api(config, "POST", `/sessions/${sessionId}/push`, {
|
|
30
|
+
html,
|
|
31
|
+
source_path: url.toString(),
|
|
32
|
+
});
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
console.log(JSON.stringify(data));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(`Pushed v${data.version} to ${sessionId} from ${url.toString()}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
printCommandError("Push URL failed", err, opts.json);
|
|
42
|
+
process.exit(1);
|
|
30
43
|
}
|
|
31
44
|
});
|
|
32
45
|
function parseHttpUrl(urlArg) {
|
|
@@ -42,26 +55,30 @@ function parseHttpUrl(urlArg) {
|
|
|
42
55
|
}
|
|
43
56
|
return url;
|
|
44
57
|
}
|
|
58
|
+
function validateSnapshotTarget(url, opts) {
|
|
59
|
+
const local = isLocalhost(url.hostname);
|
|
60
|
+
const privateNetwork = !local && isPrivateNetworkHost(url.hostname);
|
|
61
|
+
if (!local && !opts.allowRemote) {
|
|
62
|
+
throw new Error("push-url snapshots localhost by default. Pass --allow-remote --yes to upload HTML from a remote URL.");
|
|
63
|
+
}
|
|
64
|
+
if (privateNetwork && !opts.allowPrivate) {
|
|
65
|
+
throw new Error("Private-network URLs require --allow-private --yes so internal pages are not uploaded accidentally.");
|
|
66
|
+
}
|
|
67
|
+
if ((!local || privateNetwork) && !opts.yes) {
|
|
68
|
+
throw new Error("Pass --yes to confirm the fetched HTML can be uploaded to the session.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
45
71
|
async function fetchHtml(url) {
|
|
46
|
-
const controller = new AbortController();
|
|
47
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
48
72
|
let res;
|
|
49
73
|
try {
|
|
50
|
-
res = await
|
|
74
|
+
res = await fetchWithTimeout(url, {
|
|
51
75
|
headers: { Accept: "text/html,*/*;q=0.8" },
|
|
52
76
|
redirect: "follow",
|
|
53
|
-
|
|
54
|
-
});
|
|
77
|
+
}, FETCH_TIMEOUT_MS);
|
|
55
78
|
}
|
|
56
79
|
catch (err) {
|
|
57
|
-
if (err?.name === "AbortError") {
|
|
58
|
-
throw new Error(`Timed out fetching ${url.toString()} after ${FETCH_TIMEOUT_MS / 1000}s`);
|
|
59
|
-
}
|
|
60
80
|
throw err;
|
|
61
81
|
}
|
|
62
|
-
finally {
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
}
|
|
65
82
|
if (!res.ok) {
|
|
66
83
|
throw new Error(`Failed to fetch ${url.toString()}: HTTP ${res.status}`);
|
|
67
84
|
}
|
|
@@ -69,5 +86,51 @@ async function fetchHtml(url) {
|
|
|
69
86
|
if (!contentType.toLowerCase().includes("text/html")) {
|
|
70
87
|
throw new Error(`Expected text/html from ${url.toString()}, got ${contentType || "no content-type"}`);
|
|
71
88
|
}
|
|
72
|
-
|
|
89
|
+
const declaredLength = Number(res.headers.get("content-length") ?? 0);
|
|
90
|
+
if (declaredLength > MAX_HTML_BYTES) {
|
|
91
|
+
throw new Error(`HTML response is too large (${declaredLength} bytes, max ${MAX_HTML_BYTES})`);
|
|
92
|
+
}
|
|
93
|
+
return readTextWithLimit(res, MAX_HTML_BYTES);
|
|
94
|
+
}
|
|
95
|
+
async function readTextWithLimit(res, maxBytes) {
|
|
96
|
+
if (!res.body) {
|
|
97
|
+
const text = await res.text();
|
|
98
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
99
|
+
throw new Error(`HTML response is too large (max ${maxBytes} bytes)`);
|
|
100
|
+
}
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
const reader = res.body.getReader();
|
|
104
|
+
const chunks = [];
|
|
105
|
+
let total = 0;
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done)
|
|
109
|
+
break;
|
|
110
|
+
total += value.byteLength;
|
|
111
|
+
if (total > maxBytes) {
|
|
112
|
+
await reader.cancel();
|
|
113
|
+
throw new Error(`HTML response is too large (max ${maxBytes} bytes)`);
|
|
114
|
+
}
|
|
115
|
+
chunks.push(value);
|
|
116
|
+
}
|
|
117
|
+
const bytes = new Uint8Array(total);
|
|
118
|
+
let offset = 0;
|
|
119
|
+
for (const chunk of chunks) {
|
|
120
|
+
bytes.set(chunk, offset);
|
|
121
|
+
offset += chunk.byteLength;
|
|
122
|
+
}
|
|
123
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
124
|
+
}
|
|
125
|
+
function isLocalhost(hostname) {
|
|
126
|
+
const host = hostname.toLowerCase();
|
|
127
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
128
|
+
}
|
|
129
|
+
function isPrivateNetworkHost(hostname) {
|
|
130
|
+
const host = hostname.toLowerCase();
|
|
131
|
+
if (!host.includes("."))
|
|
132
|
+
return true;
|
|
133
|
+
if (host.endsWith(".local"))
|
|
134
|
+
return true;
|
|
135
|
+
return /^(10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[0-1])\.)/.test(host);
|
|
73
136
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from "fs";
|
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import { requireConfig } from "../config.js";
|
|
5
5
|
import { api } from "../api.js";
|
|
6
|
+
import { printCommandError } from "../command-error.js";
|
|
6
7
|
export const pushCommand = new Command("push")
|
|
7
8
|
.argument("<session-id>", "Session ID")
|
|
8
9
|
.argument("<file>", "Standalone HTML file path")
|
|
@@ -10,22 +11,28 @@ export const pushCommand = new Command("push")
|
|
|
10
11
|
.option("--json", "Output JSON")
|
|
11
12
|
.addHelpText("after", `
|
|
12
13
|
Use this for complete HTML documents with their own <html>/<body> shell.
|
|
13
|
-
For
|
|
14
|
+
For an HTML response snapshot, use: peekable push-url <session-id> <url>
|
|
14
15
|
For a live dev server, use: peekable proxy <port>
|
|
15
16
|
`)
|
|
16
17
|
.action(async (sessionId, file, opts) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
try {
|
|
19
|
+
const config = requireConfig();
|
|
20
|
+
const html = readFileSync(file, "utf-8");
|
|
21
|
+
if (!opts.json && looksLikeHtmlFragment(html)) {
|
|
22
|
+
console.error("\x1b[33mNote:\x1b[0m this file looks like an HTML fragment. It may render unstyled when shared standalone. If it is part of a parent app, prefer `peekable push-url <session> <url>`. For live dev servers (React/Vite/Next), use `peekable proxy <port>`.");
|
|
23
|
+
}
|
|
24
|
+
const source_path = resolve(file);
|
|
25
|
+
const data = await api(config, "POST", `/sessions/${sessionId}/push`, { html, source_path });
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify(data));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(`Pushed v${data.version} to ${sessionId}`);
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
console.log(JSON.stringify(data));
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
console.log(`Pushed v${data.version} to ${sessionId}`);
|
|
33
|
+
catch (err) {
|
|
34
|
+
printCommandError("Push failed", err, opts.json);
|
|
35
|
+
process.exit(1);
|
|
29
36
|
}
|
|
30
37
|
});
|
|
31
38
|
function looksLikeHtmlFragment(html) {
|
|
@@ -6,6 +6,7 @@ export const registerCommand = new Command("register")
|
|
|
6
6
|
.description("Register for an API key")
|
|
7
7
|
.requiredOption("--name <name>", "Your display name")
|
|
8
8
|
.requiredOption("--email <email>", "Your email address")
|
|
9
|
+
.option("--invite-code <code>", "Beta invite code")
|
|
9
10
|
.option("--url <url>", "Server URL", DEFAULT_URL)
|
|
10
11
|
.option("--json", "Output JSON")
|
|
11
12
|
.action(async (opts) => {
|
|
@@ -21,9 +22,18 @@ export const registerCommand = new Command("register")
|
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
23
24
|
try {
|
|
24
|
-
const
|
|
25
|
+
const inviteCode = typeof opts.inviteCode === "string"
|
|
26
|
+
? opts.inviteCode
|
|
27
|
+
: process.env.PEEKABLE_INVITE_CODE;
|
|
28
|
+
const body = {
|
|
25
29
|
name: opts.name,
|
|
26
30
|
email: opts.email,
|
|
31
|
+
};
|
|
32
|
+
if (inviteCode) {
|
|
33
|
+
body.invite_code = inviteCode;
|
|
34
|
+
}
|
|
35
|
+
const data = await apiNoAuth(opts.url, "POST", "/register", {
|
|
36
|
+
...body,
|
|
27
37
|
});
|
|
28
38
|
writeConfig({
|
|
29
39
|
url: opts.url,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { rmSync } from "fs";
|
|
3
|
+
type RemoveTarget = typeof rmSync;
|
|
4
|
+
type UninstallTarget = {
|
|
5
|
+
label: string;
|
|
6
|
+
path: string;
|
|
7
|
+
existed: boolean;
|
|
8
|
+
removed: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
type UninstallResult = {
|
|
12
|
+
status: "ok" | "partial";
|
|
13
|
+
targets: UninstallTarget[];
|
|
14
|
+
npmUninstallCommand: string;
|
|
15
|
+
note: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function removeLocalPeekableFiles(home?: string, removeTarget?: RemoveTarget): UninstallResult;
|
|
18
|
+
export declare function formatUninstallResult(result: UninstallResult): string;
|
|
19
|
+
export declare function createUninstallCommand(): Command;
|
|
20
|
+
export declare const uninstallCommand: Command;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync, rmSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { getClaudePeekableSkillDir, getPeekableConfigDir } from "../paths.js";
|
|
5
|
+
function getUninstallTargets(home = homedir()) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
label: "Peekable config",
|
|
9
|
+
path: getPeekableConfigDir(home),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: "Claude Code skill",
|
|
13
|
+
path: getClaudePeekableSkillDir(home),
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
export function removeLocalPeekableFiles(home = homedir(), removeTarget = rmSync) {
|
|
18
|
+
const targets = getUninstallTargets(home).map((target) => {
|
|
19
|
+
const existed = existsSync(target.path);
|
|
20
|
+
if (!existed) {
|
|
21
|
+
return { ...target, existed, removed: false };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
removeTarget(target.path, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
...target,
|
|
29
|
+
existed,
|
|
30
|
+
removed: false,
|
|
31
|
+
error: error instanceof Error ? error.message : String(error),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...target,
|
|
36
|
+
existed,
|
|
37
|
+
removed: !existsSync(target.path),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const hasFailure = targets.some((target) => target.error || (target.existed && !target.removed));
|
|
41
|
+
return {
|
|
42
|
+
status: hasFailure ? "partial" : "ok",
|
|
43
|
+
targets,
|
|
44
|
+
npmUninstallCommand: "npm uninstall -g peekable",
|
|
45
|
+
note: "This only removes local Peekable files. It does not delete hosted sessions or account data.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function formatUninstallResult(result) {
|
|
49
|
+
const lines = result.targets.map((target) => {
|
|
50
|
+
if (target.error) {
|
|
51
|
+
return `${target.label}: failed to remove (${target.path}) - ${target.error}`;
|
|
52
|
+
}
|
|
53
|
+
if (!target.existed) {
|
|
54
|
+
return `${target.label}: not found (${target.path})`;
|
|
55
|
+
}
|
|
56
|
+
return `${target.label}: ${target.removed ? "removed" : "not removed"} (${target.path})`;
|
|
57
|
+
});
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push(`To remove the CLI package, run: ${result.npmUninstallCommand}`);
|
|
60
|
+
lines.push(result.note);
|
|
61
|
+
lines.push("For hosted account or data deletion, contact support.");
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
export function createUninstallCommand() {
|
|
65
|
+
return new Command("uninstall")
|
|
66
|
+
.description("Remove local Peekable config and installed Claude Code skill")
|
|
67
|
+
.option("--json", "Output JSON")
|
|
68
|
+
.action((opts) => {
|
|
69
|
+
const result = removeLocalPeekableFiles();
|
|
70
|
+
if (opts.json) {
|
|
71
|
+
console.log(JSON.stringify(result));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(formatUninstallResult(result));
|
|
75
|
+
}
|
|
76
|
+
if (result.status !== "ok") {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export const uninstallCommand = createUninstallCommand();
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
2
|
+
import { getPeekableConfigDir, getPeekableConfigFile } from "./paths.js";
|
|
3
|
+
const CONFIG_DIR = getPeekableConfigDir();
|
|
4
|
+
const CONFIG_FILE = getPeekableConfigFile();
|
|
6
5
|
export function readConfig() {
|
|
7
6
|
if (process.env.SHARE_URL && process.env.SHARE_API_KEY) {
|
|
8
7
|
return {
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,13 @@ import { watchCommand } from "./commands/watch.js";
|
|
|
11
11
|
import { resolveCommand } from "./commands/resolve.js";
|
|
12
12
|
import { proxyCommand } from "./commands/proxy.js";
|
|
13
13
|
import { pushUrlCommand } from "./commands/push-url.js";
|
|
14
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
15
|
+
import { uninstallCommand } from "./commands/uninstall.js";
|
|
16
|
+
import { CLI_VERSION } from "./version.js";
|
|
14
17
|
program
|
|
15
18
|
.name("peekable")
|
|
16
19
|
.description("Share HTML mockups with collaborators via peekable")
|
|
17
|
-
.version(
|
|
20
|
+
.version(CLI_VERSION);
|
|
18
21
|
program.addCommand(registerCommand);
|
|
19
22
|
program.addCommand(initCommand);
|
|
20
23
|
program.addCommand(createCommand);
|
|
@@ -26,4 +29,6 @@ program.addCommand(closeCommand);
|
|
|
26
29
|
program.addCommand(watchCommand);
|
|
27
30
|
program.addCommand(resolveCommand);
|
|
28
31
|
program.addCommand(proxyCommand);
|
|
32
|
+
program.addCommand(doctorCommand);
|
|
33
|
+
program.addCommand(uninstallCommand);
|
|
29
34
|
program.parse();
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function getPeekableConfigDir(home?: string): string;
|
|
2
|
+
export declare function getPeekableConfigFile(home?: string): string;
|
|
3
|
+
export declare function getClaudeDir(home?: string): string;
|
|
4
|
+
export declare function getClaudePeekableSkillDir(home?: string): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
export function getPeekableConfigDir(home = homedir()) {
|
|
4
|
+
return join(home, ".peekable");
|
|
5
|
+
}
|
|
6
|
+
export function getPeekableConfigFile(home = homedir()) {
|
|
7
|
+
return join(getPeekableConfigDir(home), "config.json");
|
|
8
|
+
}
|
|
9
|
+
export function getClaudeDir(home = homedir()) {
|
|
10
|
+
return join(home, ".claude");
|
|
11
|
+
}
|
|
12
|
+
export function getClaudePeekableSkillDir(home = homedir()) {
|
|
13
|
+
return join(getClaudeDir(home), "skills", "peekable");
|
|
14
|
+
}
|
package/dist/quota.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatQuota(active: number | null, max: number | null, unlimited?: boolean): string;
|
package/dist/quota.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLI_VERSION = "0.1.2";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_VERSION = "0.1.2";
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -18,21 +18,21 @@ When the user wants to share an HTML file (playground, mockup, brainstorming com
|
|
|
18
18
|
1. Create a session: `peekable create "<name>" --json`
|
|
19
19
|
2. Inspect the file before pushing:
|
|
20
20
|
- If it contains `<html>` or `<body>`, push it directly: `peekable push <session-id> <file-path> --json`
|
|
21
|
-
- If it looks like an HTML fragment, do not push it blindly. If you know the
|
|
22
|
-
- If it looks like a fragment and no
|
|
21
|
+
- If it looks like an HTML fragment, do not push it blindly. If you know a localhost URL that returns the full HTML response, snapshot that instead: `peekable push-url <session-id> <url> --json`
|
|
22
|
+
- If it looks like a fragment and no localhost/page URL is known, ask for it.
|
|
23
23
|
3. Return the URL to the user
|
|
24
24
|
|
|
25
25
|
If a session already exists for this topic, reuse it (push creates a new version, collaborators auto-reload).
|
|
26
26
|
|
|
27
|
-
### Snapshot
|
|
27
|
+
### Snapshot an HTML response
|
|
28
28
|
|
|
29
|
-
When a local companion page or simple server
|
|
29
|
+
When a local companion page or simple server returns a mostly self-contained HTML response, use:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
peekable push-url <session-id> <url> --json
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`push-url` fetches the HTML response at the URL and pushes that snapshot. It does not execute JavaScript, use browser cookies, or inline external assets. Prefer `peekable proxy` for live React/Vite/Next apps with routes/assets/interactivity. It snapshots localhost by default; remote URLs require `--allow-remote --yes`, and private-network URLs also require `--allow-private`.
|
|
36
36
|
|
|
37
37
|
### Check feedback
|
|
38
38
|
|
|
@@ -72,6 +72,24 @@ peekable list --json
|
|
|
72
72
|
peekable close <session-id> --json
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
Free hosted accounts have an active-session cap. If create/proxy returns a limit error, run `peekable list --json`, close stale sessions, then retry.
|
|
76
|
+
|
|
77
|
+
### Diagnose setup
|
|
78
|
+
|
|
79
|
+
When sharing fails, auth looks broken, or a user asks for help debugging:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
peekable doctor --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For a deeper check that creates, pushes, and closes a temporary session:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
peekable doctor --test-push --json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Doctor output is designed to be safe for support: it does not include API keys, HTML payloads, or annotation text.
|
|
92
|
+
|
|
75
93
|
### Watch for annotations
|
|
76
94
|
|
|
77
95
|
Start a background listener for annotation notifications:
|