@mogulmoretti/skrape 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/LICENSE +32 -0
- package/README.md +117 -0
- package/dist/auth/session.d.ts +26 -0
- package/dist/auth/session.js +70 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +123 -0
- package/dist/discover/communities.d.ts +22 -0
- package/dist/discover/communities.js +92 -0
- package/dist/discover/skool.d.ts +17 -0
- package/dist/discover/skool.js +91 -0
- package/dist/fetch/browser.d.ts +16 -0
- package/dist/fetch/browser.js +58 -0
- package/dist/fetch/chromeSetup.d.ts +54 -0
- package/dist/fetch/chromeSetup.js +126 -0
- package/dist/fetch/http.d.ts +17 -0
- package/dist/fetch/http.js +64 -0
- package/dist/fetch/nextdata.d.ts +9 -0
- package/dist/fetch/nextdata.js +23 -0
- package/dist/fetch/resilient.d.ts +12 -0
- package/dist/fetch/resilient.js +68 -0
- package/dist/media/index.d.ts +4 -0
- package/dist/media/index.js +11 -0
- package/dist/media/loom.d.ts +5 -0
- package/dist/media/loom.js +60 -0
- package/dist/normalize/vtt.d.ts +12 -0
- package/dist/normalize/vtt.js +109 -0
- package/dist/store/db.d.ts +15 -0
- package/dist/store/db.js +125 -0
- package/dist/store/markdown.d.ts +7 -0
- package/dist/store/markdown.js +41 -0
- package/dist/sync.d.ts +30 -0
- package/dist/sync.js +178 -0
- package/dist/tui/App.d.ts +22 -0
- package/dist/tui/App.js +285 -0
- package/dist/tui/SelectList.d.ts +14 -0
- package/dist/tui/SelectList.js +25 -0
- package/dist/tui/browserLifecycle.d.ts +71 -0
- package/dist/tui/browserLifecycle.js +114 -0
- package/dist/tui/chromeSetup.d.ts +50 -0
- package/dist/tui/chromeSetup.js +52 -0
- package/dist/tui/flow.d.ts +53 -0
- package/dist/tui/flow.js +30 -0
- package/dist/tui/progress.d.ts +21 -0
- package/dist/tui/progress.js +31 -0
- package/dist/tui/reveal.d.ts +17 -0
- package/dist/tui/reveal.js +47 -0
- package/dist/tui/run.d.ts +12 -0
- package/dist/tui/run.js +96 -0
- package/dist/tui/slug.d.ts +7 -0
- package/dist/tui/slug.js +18 -0
- package/dist/tui/summary.d.ts +7 -0
- package/dist/tui/summary.js +29 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
package/dist/tui/flow.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const MENU_ITEMS = [
|
|
2
|
+
{ label: 'Sync another community', value: 'sync-another' },
|
|
3
|
+
{ label: 'Sync this community again', value: 'sync-again' },
|
|
4
|
+
{ label: 'Open output folder', value: 'open-folder' },
|
|
5
|
+
{ label: 'Quit', value: 'quit' },
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* Given the state of the sync that just completed and the menu choice the
|
|
9
|
+
* user made, returns the next step transition — or `null` when the choice
|
|
10
|
+
* doesn't move to a new screen at all: `'open-folder'` is a side effect
|
|
11
|
+
* performed in place (the menu stays up), and `'quit'` exits instead of
|
|
12
|
+
* transitioning to another step.
|
|
13
|
+
*/
|
|
14
|
+
export function nextStepForMenuChoice(done, choice) {
|
|
15
|
+
switch (choice) {
|
|
16
|
+
case 'sync-another':
|
|
17
|
+
return { kind: 'discovering' };
|
|
18
|
+
case 'sync-again':
|
|
19
|
+
return {
|
|
20
|
+
kind: 'confirming',
|
|
21
|
+
slug: done.slug,
|
|
22
|
+
name: done.name,
|
|
23
|
+
courseCount: done.courseCount,
|
|
24
|
+
outDir: done.outDir,
|
|
25
|
+
};
|
|
26
|
+
case 'open-folder':
|
|
27
|
+
case 'quit':
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Outcome, ProgressEvent } from '../sync.js';
|
|
2
|
+
export interface ProgressState {
|
|
3
|
+
total: number;
|
|
4
|
+
done: number;
|
|
5
|
+
counts: Record<Outcome, number>;
|
|
6
|
+
current: {
|
|
7
|
+
course: string;
|
|
8
|
+
title: string;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function createProgressState(total: number): ProgressState;
|
|
12
|
+
/**
|
|
13
|
+
* Pure reducer folding one onProgress event into state. Events arrive out of
|
|
14
|
+
* order from concurrent workers, so `done`/`total` are taken straight from the
|
|
15
|
+
* event (sync.ts already owns and increments that counter atomically) rather
|
|
16
|
+
* than re-derived here, and `current` is simply whichever event was folded in
|
|
17
|
+
* most recently — a stable "last seen" snapshot, not a claim about true
|
|
18
|
+
* completion order.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyProgress(state: ProgressState, event: ProgressEvent): ProgressState;
|
|
21
|
+
export declare function formatProgressBar(done: number, total: number, width?: number): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createProgressState(total) {
|
|
2
|
+
return {
|
|
3
|
+
total,
|
|
4
|
+
done: 0,
|
|
5
|
+
counts: { ok: 0, skipped: 0, 'no-video': 0, 'no-access': 0, unavailable: 0, failed: 0 },
|
|
6
|
+
current: null,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Pure reducer folding one onProgress event into state. Events arrive out of
|
|
11
|
+
* order from concurrent workers, so `done`/`total` are taken straight from the
|
|
12
|
+
* event (sync.ts already owns and increments that counter atomically) rather
|
|
13
|
+
* than re-derived here, and `current` is simply whichever event was folded in
|
|
14
|
+
* most recently — a stable "last seen" snapshot, not a claim about true
|
|
15
|
+
* completion order.
|
|
16
|
+
*/
|
|
17
|
+
export function applyProgress(state, event) {
|
|
18
|
+
return {
|
|
19
|
+
total: event.total,
|
|
20
|
+
done: event.done,
|
|
21
|
+
counts: { ...state.counts, [event.outcome]: state.counts[event.outcome] + 1 },
|
|
22
|
+
current: { course: event.course, title: event.title },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function formatProgressBar(done, total, width = 24) {
|
|
26
|
+
const safeTotal = total > 0 ? total : 1;
|
|
27
|
+
const ratio = Math.min(1, Math.max(0, done / safeTotal));
|
|
28
|
+
const filled = Math.round(ratio * width);
|
|
29
|
+
const bar = '#'.repeat(filled) + '-'.repeat(width - filled);
|
|
30
|
+
return `[${bar}] ${done}/${total}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RevealDeps {
|
|
2
|
+
/** Injectable so tests never spawn a real process. Defaults to
|
|
3
|
+
* node:child_process's execFile, promisified. */
|
|
4
|
+
run: (command: string, args: string[]) => Promise<void>;
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
/** Where to print the path when reveal isn't possible/fails. Defaults to
|
|
7
|
+
* console.log. */
|
|
8
|
+
log: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function defaultRevealDeps(): RevealDeps;
|
|
11
|
+
/**
|
|
12
|
+
* Reveals `outDir` in the OS file manager. Tries the platform-appropriate
|
|
13
|
+
* command; if there isn't one for this platform, or the command fails (not
|
|
14
|
+
* installed, headless environment, etc.), this never throws — it just prints
|
|
15
|
+
* the path instead, so a failed reveal can never crash the TUI.
|
|
16
|
+
*/
|
|
17
|
+
export declare function revealOutputFolder(outDir: string, deps?: RevealDeps): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
function defaultRun(command, args) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
execFile(command, args, (error) => {
|
|
5
|
+
if (error)
|
|
6
|
+
reject(error);
|
|
7
|
+
else
|
|
8
|
+
resolve();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function defaultRevealDeps() {
|
|
13
|
+
return { run: defaultRun, platform: process.platform, log: (message) => console.log(message) };
|
|
14
|
+
}
|
|
15
|
+
/** Maps a platform to the OS command that reveals a folder in the file
|
|
16
|
+
* manager, or null when there's no known one to try. */
|
|
17
|
+
function revealCommandFor(platform) {
|
|
18
|
+
switch (platform) {
|
|
19
|
+
case 'darwin':
|
|
20
|
+
return { command: 'open', args: (path) => [path] };
|
|
21
|
+
case 'win32':
|
|
22
|
+
return { command: 'explorer', args: (path) => [path] };
|
|
23
|
+
case 'linux':
|
|
24
|
+
return { command: 'xdg-open', args: (path) => [path] };
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reveals `outDir` in the OS file manager. Tries the platform-appropriate
|
|
31
|
+
* command; if there isn't one for this platform, or the command fails (not
|
|
32
|
+
* installed, headless environment, etc.), this never throws — it just prints
|
|
33
|
+
* the path instead, so a failed reveal can never crash the TUI.
|
|
34
|
+
*/
|
|
35
|
+
export async function revealOutputFolder(outDir, deps = defaultRevealDeps()) {
|
|
36
|
+
const reveal = revealCommandFor(deps.platform);
|
|
37
|
+
if (!reveal) {
|
|
38
|
+
deps.log(outDir);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await deps.run(reveal.command, reveal.args(outDir));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
deps.log(outDir);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the guided, no-arguments flow: session check, community picker,
|
|
3
|
+
* confirmation, live sync progress, and a result summary. Owns the db and
|
|
4
|
+
* the browser lifecycle for the whole flow and guarantees both are closed
|
|
5
|
+
* on every exit path — normal completion, an in-flow error, or Ctrl+C.
|
|
6
|
+
*
|
|
7
|
+
* Browser lifetime is delegated to `createBrowserLifecycle` — see the
|
|
8
|
+
* invariant documented there: only one Chrome process may ever hold the
|
|
9
|
+
* profile directory at a time. That's what let the login step below run
|
|
10
|
+
* "for free" here without any direct knowledge of when browsers open or close.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runGuidedFlow(): Promise<void>;
|
package/dist/tui/run.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { HttpFetcher } from '../fetch/http.js';
|
|
4
|
+
import { BrowserFetcher } from '../fetch/browser.js';
|
|
5
|
+
import { isChromeMarkedInstalled, installChromeViaCli, markChromeInstalled, probeChromeLaunchable } from '../fetch/chromeSetup.js';
|
|
6
|
+
import { openDb } from '../store/db.js';
|
|
7
|
+
import { syncClassroom } from '../sync.js';
|
|
8
|
+
import { chromeMarkerPath, dbPath, ensureRoot, isLoggedIn, login, profileDir } from '../auth/session.js';
|
|
9
|
+
import { listCourses } from '../discover/skool.js';
|
|
10
|
+
import { listUserCommunities } from '../discover/communities.js';
|
|
11
|
+
import { App } from './App.js';
|
|
12
|
+
import { createBrowserLifecycle } from './browserLifecycle.js';
|
|
13
|
+
import { ensureChromeReady } from './chromeSetup.js';
|
|
14
|
+
const OUT_ROOT = './out';
|
|
15
|
+
/**
|
|
16
|
+
* Runs the guided, no-arguments flow: session check, community picker,
|
|
17
|
+
* confirmation, live sync progress, and a result summary. Owns the db and
|
|
18
|
+
* the browser lifecycle for the whole flow and guarantees both are closed
|
|
19
|
+
* on every exit path — normal completion, an in-flow error, or Ctrl+C.
|
|
20
|
+
*
|
|
21
|
+
* Browser lifetime is delegated to `createBrowserLifecycle` — see the
|
|
22
|
+
* invariant documented there: only one Chrome process may ever hold the
|
|
23
|
+
* profile directory at a time. That's what let the login step below run
|
|
24
|
+
* "for free" here without any direct knowledge of when browsers open or close.
|
|
25
|
+
*/
|
|
26
|
+
export async function runGuidedFlow() {
|
|
27
|
+
await ensureRoot();
|
|
28
|
+
// First-run browser setup: must complete before the session check below,
|
|
29
|
+
// and before the db/browser lifecycle are created — a failure here leaves
|
|
30
|
+
// nothing to clean up and is safe to retry on the next invocation.
|
|
31
|
+
await ensureChromeReady({
|
|
32
|
+
isConfirmedInstalled: () => isChromeMarkedInstalled(chromeMarkerPath()),
|
|
33
|
+
probeLaunchable: probeChromeLaunchable,
|
|
34
|
+
markInstalled: (executablePath) => markChromeInstalled(chromeMarkerPath(), executablePath),
|
|
35
|
+
installChrome: async (onProgress) => {
|
|
36
|
+
console.log('\nSetting up the browser skrape needs — this happens once, ~150MB.\n');
|
|
37
|
+
await installChromeViaCli(onProgress);
|
|
38
|
+
console.log('\nBrowser setup complete.\n');
|
|
39
|
+
const executablePath = await probeChromeLaunchable();
|
|
40
|
+
if (!executablePath) {
|
|
41
|
+
throw new Error('Chrome installed, but still could not be launched. This usually means a platform-specific ' +
|
|
42
|
+
'dependency is missing — see the Playwright install output above for details.');
|
|
43
|
+
}
|
|
44
|
+
return executablePath;
|
|
45
|
+
},
|
|
46
|
+
}, (elapsedSeconds) => {
|
|
47
|
+
process.stdout.write(`\r installing… ${elapsedSeconds}s elapsed`);
|
|
48
|
+
});
|
|
49
|
+
const db = openDb(dbPath());
|
|
50
|
+
const lifecycle = createBrowserLifecycle({
|
|
51
|
+
http: new HttpFetcher(),
|
|
52
|
+
isLoggedIn,
|
|
53
|
+
makeBrowserFetcher: () => new BrowserFetcher(profileDir()),
|
|
54
|
+
performLogin: (onContext) => login(onContext),
|
|
55
|
+
});
|
|
56
|
+
let closed = false;
|
|
57
|
+
const cleanup = async () => {
|
|
58
|
+
if (closed)
|
|
59
|
+
return;
|
|
60
|
+
closed = true;
|
|
61
|
+
try {
|
|
62
|
+
await lifecycle.cleanup();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`\nWarning: failed to close browser cleanly: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
db.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const onSigint = () => {
|
|
72
|
+
void cleanup().finally(() => process.exit(130));
|
|
73
|
+
};
|
|
74
|
+
process.on('SIGINT', onSigint);
|
|
75
|
+
const controllers = {
|
|
76
|
+
outRoot: OUT_ROOT,
|
|
77
|
+
checkLoggedIn: async () => lifecycle.checkLoggedIn(),
|
|
78
|
+
login: async () => {
|
|
79
|
+
await lifecycle.login();
|
|
80
|
+
},
|
|
81
|
+
discoverCommunities: async () => listUserCommunities(lifecycle.getFetcher()),
|
|
82
|
+
countAccessibleCourses: async (slug) => {
|
|
83
|
+
const courses = await listCourses(slug, lifecycle.getFetcher());
|
|
84
|
+
return courses.filter((course) => course.hasAccess).length;
|
|
85
|
+
},
|
|
86
|
+
runSync: async (slug, outDir, onProgress) => syncClassroom({ slug, outDir, db, fetcher: lifecycle.getFetcher(), concurrency: 4, onProgress }),
|
|
87
|
+
};
|
|
88
|
+
const { waitUntilExit } = render(React.createElement(App, { controllers }));
|
|
89
|
+
try {
|
|
90
|
+
await waitUntilExit();
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
process.off('SIGINT', onSigint);
|
|
94
|
+
await cleanup();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips a pasted skool.com URL (or a path within one) down to the bare slug,
|
|
3
|
+
* and trims whitespace. Users are told to enter "the part after skool.com/",
|
|
4
|
+
* but pasting the whole URL is common enough to normalize rather than reject.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeSlug(input: string): string;
|
|
7
|
+
export declare function isValidSlug(input: string): boolean;
|
package/dist/tui/slug.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const SLUG_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,98}[a-zA-Z0-9])?$/;
|
|
2
|
+
/**
|
|
3
|
+
* Strips a pasted skool.com URL (or a path within one) down to the bare slug,
|
|
4
|
+
* and trims whitespace. Users are told to enter "the part after skool.com/",
|
|
5
|
+
* but pasting the whole URL is common enough to normalize rather than reject.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeSlug(input) {
|
|
8
|
+
let value = input.trim();
|
|
9
|
+
value = value.replace(/^https?:\/\//i, '');
|
|
10
|
+
value = value.replace(/^(www\.)?skool\.com\//i, '');
|
|
11
|
+
const slashIndex = value.indexOf('/');
|
|
12
|
+
if (slashIndex !== -1)
|
|
13
|
+
value = value.slice(0, slashIndex);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function isValidSlug(input) {
|
|
17
|
+
return SLUG_RE.test(input);
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SyncSummary } from '../sync.js';
|
|
2
|
+
export declare function formatCountLines(summary: SyncSummary): string[];
|
|
3
|
+
export declare function formatProblemLines(summary: SyncSummary): string[];
|
|
4
|
+
/** True when nothing was transcribed at all — the case that must never be
|
|
5
|
+
* rendered as a cheerful, empty-looking summary. */
|
|
6
|
+
export declare function nothingTranscribed(summary: SyncSummary): boolean;
|
|
7
|
+
export declare function formatSummary(summary: SyncSummary, outDir: string): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function formatCountLines(summary) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
for (const [outcome, count] of Object.entries(summary.counts)) {
|
|
4
|
+
if (count > 0)
|
|
5
|
+
lines.push(` ${outcome.padEnd(14)} ${count}`);
|
|
6
|
+
}
|
|
7
|
+
return lines;
|
|
8
|
+
}
|
|
9
|
+
export function formatProblemLines(summary) {
|
|
10
|
+
return summary.problems.map((problem) => ` [${problem.outcome}] ${problem.course} / ${problem.title}: ${problem.reason}`);
|
|
11
|
+
}
|
|
12
|
+
/** True when nothing was transcribed at all — the case that must never be
|
|
13
|
+
* rendered as a cheerful, empty-looking summary. */
|
|
14
|
+
export function nothingTranscribed(summary) {
|
|
15
|
+
return summary.counts.ok === 0 && summary.totalWords === 0;
|
|
16
|
+
}
|
|
17
|
+
export function formatSummary(summary, outDir) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
if (nothingTranscribed(summary)) {
|
|
20
|
+
lines.push('Nothing was transcribed.', '');
|
|
21
|
+
}
|
|
22
|
+
lines.push('--- summary ---', ...formatCountLines(summary));
|
|
23
|
+
lines.push(` ${'words'.padEnd(14)} ${summary.totalWords.toLocaleString('en-US')}`);
|
|
24
|
+
if (summary.problems.length > 0) {
|
|
25
|
+
lines.push('', '--- not transcribed ---', ...formatProblemLines(summary));
|
|
26
|
+
}
|
|
27
|
+
lines.push('', `Output: ${outDir}`);
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ItemType = 'lesson' | 'call' | 'post';
|
|
2
|
+
export interface ContentItem {
|
|
3
|
+
nativeId: string;
|
|
4
|
+
type: ItemType;
|
|
5
|
+
title: string;
|
|
6
|
+
index: number;
|
|
7
|
+
course: string | null;
|
|
8
|
+
section: string | null;
|
|
9
|
+
url: string | null;
|
|
10
|
+
videoUrl: string | null;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
hasAccess: boolean;
|
|
13
|
+
publishedAt: string | null;
|
|
14
|
+
bodyText: string | null;
|
|
15
|
+
}
|
|
16
|
+
export type TranscriptResult = {
|
|
17
|
+
status: 'ok';
|
|
18
|
+
text: string;
|
|
19
|
+
wordCount: number;
|
|
20
|
+
provider: string;
|
|
21
|
+
sourceUrl: string;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'unavailable';
|
|
24
|
+
reason: string;
|
|
25
|
+
provider: string;
|
|
26
|
+
} | {
|
|
27
|
+
status: 'failed';
|
|
28
|
+
reason: string;
|
|
29
|
+
provider: string;
|
|
30
|
+
};
|
|
31
|
+
export interface Fetcher {
|
|
32
|
+
getPage(url: string): Promise<string>;
|
|
33
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mogulmoretti/skrape",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read Skool course content instead of watching it — pulls the classroom of a community you belong to into clean, readable transcripts.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"skool",
|
|
7
|
+
"cli",
|
|
8
|
+
"transcript",
|
|
9
|
+
"video-to-text",
|
|
10
|
+
"loom",
|
|
11
|
+
"course"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/slyngg/skrape.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/slyngg/skrape/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/slyngg/skrape#readme",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"skrape": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc && chmod +x dist/cli.js",
|
|
33
|
+
"prepublishOnly": "npm run build && npm test",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"better-sqlite3": "^11.5.0",
|
|
42
|
+
"commander": "^12.1.0",
|
|
43
|
+
"ink": "^7.1.1",
|
|
44
|
+
"react": "^19.2.8",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
49
|
+
"@types/node": "^22.9.0",
|
|
50
|
+
"@types/react": "^19.2.18",
|
|
51
|
+
"typescript": "^5.6.3",
|
|
52
|
+
"vitest": "^2.1.4"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"playwright": "^1.48.2"
|
|
56
|
+
}
|
|
57
|
+
}
|