extraktr 1.0.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.
Files changed (3) hide show
  1. package/README.md +90 -0
  2. package/index.js +347 -0
  3. package/package.json +17 -0
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Extraktr CLI
2
+
3
+ Installable Node package that calls **`POST /api/extract`** on the Extraktr frontend (same route as the web app). The server proxy may attach session-derived `Authorization` for browser traffic; the CLI uses optional bearer JWTs or anonymous access when the deployment allows it.
4
+
5
+ This package is **npm-ready** (valid `package.json`, `bin`, and publish `files`). A **public npm publish** may happen later; this README does not assume the package is on the public registry yet.
6
+
7
+ ## Requirements
8
+
9
+ - **Node.js 18+** (global `fetch`)
10
+
11
+ ## Install options
12
+
13
+ ### From this repo (local development)
14
+
15
+ ```bash
16
+ cd cli
17
+ npm install
18
+ ```
19
+
20
+ Run with Node:
21
+
22
+ ```bash
23
+ node index.js extract --file ./sample-simple.txt
24
+ node index.js --help
25
+ node index.js --version
26
+ ```
27
+
28
+ ### Global-style command on your machine (`extraktr`)
29
+
30
+ From `cli/`:
31
+
32
+ ```bash
33
+ npm link
34
+ ```
35
+
36
+ Then:
37
+
38
+ ```bash
39
+ extraktr extract --file ./sample-simple.txt
40
+ extraktr --help
41
+ extraktr extract --help
42
+ extraktr --version
43
+ ```
44
+
45
+ To remove the link later: `npm unlink -g extraktr-cli` (package name) or follow npm’s unlink docs for your setup.
46
+
47
+ After a future **public publish**, `npx extraktr-cli …` or a scoped name chosen at publish time would work the same way as any other npm binary; until then, use repo paths or `npm link`.
48
+
49
+ ## Environment
50
+
51
+ | Variable | Purpose |
52
+ |----------|---------|
53
+ | `EXTRAKTR_BASE_URL` | Site origin (default `https://extraktr.com`). Requests go to `${EXTRAKTR_BASE_URL}/api/extract`. |
54
+ | `EXTRAKTR_BEARER` | Optional `Authorization: Bearer <jwt>` (must be a token the API already accepts). |
55
+ | `EXTRAKTR_FORMAT` | Default for `--format`: `text`, `json`, or `markdown` (default `text`). |
56
+
57
+ ## Usage
58
+
59
+ Use **exactly one** input method:
60
+
61
+ - `--file <path>`
62
+ - `--stdin` (pipe text in; fails if stdin is a TTY with no pipe)
63
+
64
+ ### Examples
65
+
66
+ ```bash
67
+ extraktr extract --file ./sample-simple.txt
68
+ Get-Content sample-simple.txt | extraktr extract --stdin
69
+ extraktr extract --file ./sample-simple.txt --format json
70
+ extraktr extract --file ./sample-simple.txt --format json --output out.json
71
+ extraktr extract --file ./thread.txt --source slack
72
+ EXTRAKTR_BEARER="Bearer …" extraktr extract --file ./thread.txt
73
+ ```
74
+
75
+ With **`--format json`**, successful runs write **only JSON to stdout** so you can pipe it. Diagnostics (e.g. `POST https://…`) go to **stderr** for non-JSON formats; for JSON they are omitted on success. Errors are written to **stderr**.
76
+
77
+ The frontend proxy forwards **`X-Forwarded-For`** and **`X-Forwarded-Proto`** from your request when applicable.
78
+
79
+ ## Troubleshooting
80
+
81
+ - **`unknown command`** — Only `extract` is supported; run `extraktr --help`.
82
+ - **`use either --file or --stdin`** — You passed both; use one.
83
+ - **`File not found`** — Check the path to `--file`.
84
+ - **`--stdin requires piped input`** — Pipe file content into the process (e.g. PowerShell `Get-Content … | extraktr extract --stdin`).
85
+ - **HTTP 401/403** — Deployment may require auth; try `EXTRAKTR_BEARER` or `--bearer` with a valid JWT.
86
+ - **Network errors** — Check `EXTRAKTR_BASE_URL` and connectivity to the frontend.
87
+
88
+ ## License
89
+
90
+ MIT (see `package.json`).
package/index.js ADDED
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Extraktr CLI — calls the same POST /api/extract contract as the web app (via the frontend proxy).
5
+ *
6
+ * Auth:
7
+ * - Browser sessions use cookies; the CLI cannot. Anonymous extraction works when the deployment allows it.
8
+ * - Optional EXTRAKTR_BEARER or --bearer <token>: forwarded as Authorization (same JWT shape the backend
9
+ * accepts from the session bridge). Obtaining that token is not part of this package.
10
+ *
11
+ * Env:
12
+ * - EXTRAKTR_BASE_URL — e.g. https://extraktr.com (default). Request goes to ${EXTRAKTR_BASE_URL}/api/extract
13
+ * - EXTRAKTR_FORMAT — default for --format: text | json | markdown
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const DEFAULT_BASE = "https://extraktr.com";
20
+
21
+ function readPackageVersion() {
22
+ try {
23
+ const pkg = require(path.join(__dirname, "package.json"));
24
+ return typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : "0.0.0";
25
+ } catch {
26
+ return "0.0.0";
27
+ }
28
+ }
29
+
30
+ const CLI_VERSION = readPackageVersion();
31
+
32
+ function printGlobalHelp() {
33
+ process.stdout.write(`Extraktr CLI ${CLI_VERSION} — terminal client for the Extraktr extraction API.
34
+
35
+ Usage:
36
+ extraktr extract --file <path> [options]
37
+ extraktr extract --stdin [options] # pipe conversation text
38
+
39
+ Commands:
40
+ extract Run extraction (same contract as POST /api/extract on the frontend)
41
+
42
+ Global options:
43
+ --help, -h Show this message
44
+ --version, -v Show package version
45
+
46
+ Run: extraktr extract --help (flags, env vars, input rules)
47
+ `);
48
+ }
49
+
50
+ function printExtractHelp() {
51
+ process.stdout.write(`
52
+ Usage:
53
+ extraktr extract --file <path> [options]
54
+ extraktr extract --stdin [options] # pipe conversation text on stdin
55
+
56
+ Input (exactly one):
57
+ --file <path> Read conversation text from a file
58
+ --stdin Read from piped stdin (not a TTY)
59
+
60
+ Environment:
61
+ EXTRAKTR_BASE_URL Frontend origin (default: ${DEFAULT_BASE})
62
+ EXTRAKTR_BEARER Optional Authorization: Bearer <jwt>
63
+ EXTRAKTR_FORMAT Default --format: text | json | markdown (default: text)
64
+
65
+ Options:
66
+ --source <type> Optional source_type: slack|gmail|discord|teams|generic
67
+ --format <fmt> Output: text | json | markdown
68
+ --output <path> Write output to file (same format as --format)
69
+ --bearer <token> Overrides EXTRAKTR_BEARER for this run
70
+ --help, -h Show this help
71
+
72
+ Notes:
73
+ Do not combine --file and --stdin. With --format json, stdout is valid JSON only on success (errors on stderr).
74
+
75
+ Examples:
76
+ extraktr extract --file sample.txt
77
+ Get-Content sample.txt | extraktr extract --stdin
78
+ extraktr extract --file sample.txt --format json
79
+ extraktr extract --file sample.txt --format json --output out.json
80
+ `);
81
+ }
82
+ const VALID_FORMATS = new Set(["text", "json", "markdown"]);
83
+
84
+ function defaultFormat() {
85
+ const e = process.env.EXTRAKTR_FORMAT?.trim().toLowerCase();
86
+ if (e && VALID_FORMATS.has(e)) return e;
87
+ return "text";
88
+ }
89
+
90
+ function parseArgs(argv) {
91
+ const args = argv.slice(2);
92
+ const out = {
93
+ bearer: process.env.EXTRAKTR_BEARER?.trim() || null,
94
+ source: null,
95
+ file: null,
96
+ stdin: false,
97
+ format: defaultFormat(),
98
+ output: null,
99
+ };
100
+ let i = 0;
101
+ if (args[0] === "extract") {
102
+ i = 1;
103
+ }
104
+ for (; i < args.length; i++) {
105
+ const a = args[i];
106
+ if (a === "--help" || a === "-h") out.help = true;
107
+ else if (a === "--stdin") out.stdin = true;
108
+ else if (a === "--file" && args[i + 1]) {
109
+ out.file = args[++i];
110
+ } else if (a === "--format" && args[i + 1]) {
111
+ out.format = args[++i].trim().toLowerCase();
112
+ } else if (a === "--output" && args[i + 1]) {
113
+ out.output = args[++i];
114
+ } else if (a === "--bearer" && args[i + 1]) {
115
+ out.bearer = args[++i].trim();
116
+ } else if (a === "--source" && args[i + 1]) {
117
+ out.source = args[++i].trim();
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+
123
+ function extractUrl() {
124
+ const base = (process.env.EXTRAKTR_BASE_URL || DEFAULT_BASE).replace(/\/+$/, "");
125
+ return `${base}/api/extract`;
126
+ }
127
+
128
+ function itemText(x) {
129
+ if (x == null) return "";
130
+ if (typeof x === "string") return x;
131
+ if (typeof x.text === "string") return x.text;
132
+ return String(x);
133
+ }
134
+
135
+ function readStdin() {
136
+ return new Promise((resolve, reject) => {
137
+ const chunks = [];
138
+ process.stdin.on("data", (c) => chunks.push(c));
139
+ process.stdin.on("end", () => {
140
+ try {
141
+ resolve(Buffer.concat(chunks).toString("utf8"));
142
+ } catch (e) {
143
+ reject(e);
144
+ }
145
+ });
146
+ process.stdin.on("error", reject);
147
+ });
148
+ }
149
+
150
+ function renderText(data) {
151
+ const lines = [];
152
+ lines.push("=== SUMMARY ===");
153
+ lines.push(data.summary || "(none)");
154
+ lines.push("");
155
+ lines.push("=== TASKS ===");
156
+ (data.tasks || []).forEach((t) => {
157
+ lines.push(`- ${itemText(t)}`);
158
+ });
159
+ lines.push("");
160
+ lines.push("=== DECISIONS ===");
161
+ (data.decisions || []).forEach((d) => {
162
+ lines.push(`- ${itemText(d)}`);
163
+ });
164
+ lines.push("");
165
+ lines.push("=== RISKS ===");
166
+ (data.risks || []).forEach((r) => {
167
+ lines.push(`- ${itemText(r)}`);
168
+ });
169
+ return lines.join("\n");
170
+ }
171
+
172
+ function renderMarkdown(data) {
173
+ const lines = [];
174
+ lines.push("## Summary", "", (data.summary || "(none)").trim(), "");
175
+ lines.push("## Tasks", "");
176
+ (data.tasks || []).forEach((t) => lines.push(`- ${itemText(t)}`));
177
+ lines.push("", "## Decisions", "");
178
+ (data.decisions || []).forEach((d) => lines.push(`- ${itemText(d)}`));
179
+ lines.push("", "## Risks", "");
180
+ (data.risks || []).forEach((r) => lines.push(`- ${itemText(r)}`));
181
+ return lines.join("\n");
182
+ }
183
+
184
+ function renderOutput(data, format) {
185
+ if (format === "json") {
186
+ return `${JSON.stringify(data, null, 2)}\n`;
187
+ }
188
+ if (format === "markdown") {
189
+ return `${renderMarkdown(data)}\n`;
190
+ }
191
+ return `${renderText(data)}\n`;
192
+ }
193
+
194
+ async function resolveInput(opts) {
195
+ if (opts.file && opts.stdin) {
196
+ return {
197
+ error:
198
+ "Error: use either --file <path> or --stdin, not both.",
199
+ };
200
+ }
201
+ if (opts.file) {
202
+ if (!fs.existsSync(opts.file)) {
203
+ return { error: `Error: File not found: ${opts.file}` };
204
+ }
205
+ return { content: fs.readFileSync(opts.file, "utf-8") };
206
+ }
207
+ if (opts.stdin) {
208
+ if (process.stdin.isTTY) {
209
+ return {
210
+ error:
211
+ "Error: --stdin requires piped input (stdin is a TTY). Example: Get-Content sample.txt | extraktr extract --stdin",
212
+ };
213
+ }
214
+ const content = await readStdin();
215
+ if (content.length === 0) {
216
+ return { error: "Error: stdin was empty; pipe conversation text into stdin." };
217
+ }
218
+ return { content };
219
+ }
220
+ return {
221
+ error:
222
+ "Error: provide input with --file <path> or --stdin (pipe). Pass --help for usage.",
223
+ };
224
+ }
225
+
226
+ async function run() {
227
+ const argv = process.argv.slice(2);
228
+
229
+ if (argv.length === 0) {
230
+ printGlobalHelp();
231
+ process.exit(0);
232
+ }
233
+
234
+ const first = argv[0];
235
+ if (first === "--version" || first === "-v") {
236
+ process.stdout.write(`${CLI_VERSION}\n`);
237
+ process.exit(0);
238
+ }
239
+ if (first === "--help" || first === "-h") {
240
+ printGlobalHelp();
241
+ process.exit(0);
242
+ }
243
+
244
+ if (first !== "extract") {
245
+ console.error(`Error: unknown command "${first}". The only command is: extract`);
246
+ console.error("Run: extraktr --help");
247
+ process.exit(1);
248
+ }
249
+
250
+ const opts = parseArgs(process.argv);
251
+ if (opts.help) {
252
+ printExtractHelp();
253
+ process.exit(0);
254
+ }
255
+
256
+ if (!VALID_FORMATS.has(opts.format)) {
257
+ console.error(`Error: invalid --format "${opts.format}". Use: text, json, markdown`);
258
+ process.exit(1);
259
+ }
260
+
261
+ const input = await resolveInput(opts);
262
+ if (input.error) {
263
+ console.error(input.error);
264
+ process.exit(1);
265
+ }
266
+
267
+ const content = input.content;
268
+ const API_URL = extractUrl();
269
+
270
+ const payload = { raw_content: content };
271
+ if (opts.source) payload.source_type = opts.source;
272
+
273
+ const headers = {
274
+ "Content-Type": "application/json",
275
+ Accept: "application/json",
276
+ "User-Agent": `ExtraktrCLI/${CLI_VERSION} (+https://extraktr.com)`,
277
+ };
278
+ if (opts.bearer) {
279
+ headers.Authorization = opts.bearer.startsWith("Bearer ") ? opts.bearer : `Bearer ${opts.bearer}`;
280
+ }
281
+
282
+ if (opts.format !== "json") {
283
+ console.error("POST", API_URL);
284
+ }
285
+
286
+ let res;
287
+ try {
288
+ res = await fetch(API_URL, {
289
+ method: "POST",
290
+ headers,
291
+ body: JSON.stringify(payload),
292
+ });
293
+ } catch (err) {
294
+ console.error("Extraction failed (network):", err.message || err);
295
+ process.exit(1);
296
+ }
297
+
298
+ const rawText = await res.text();
299
+ let data = null;
300
+ try {
301
+ data = rawText ? JSON.parse(rawText) : null;
302
+ } catch {
303
+ data = null;
304
+ }
305
+
306
+ if (!res.ok) {
307
+ const msg =
308
+ data && typeof data.message === "string"
309
+ ? data.message
310
+ : data && typeof data.detail === "string"
311
+ ? data.detail
312
+ : rawText.slice(0, 500);
313
+ const code = data && data.code ? ` [${data.code}]` : "";
314
+ console.error(`Extraction failed: HTTP ${res.status}${code}`);
315
+ if (msg) console.error(msg);
316
+ if (res.status === 401 || res.status === 403) {
317
+ console.error(
318
+ "Hint: Anonymous access may be blocked or rate-limited; signed-in browser traffic uses a session. Optional bearer JWT: EXTRAKTR_BEARER or --bearer."
319
+ );
320
+ }
321
+ process.exit(1);
322
+ }
323
+
324
+ if (!data || typeof data !== "object") {
325
+ console.error("Extraction failed: response was not JSON");
326
+ console.error(rawText.slice(0, 500));
327
+ process.exit(1);
328
+ }
329
+
330
+ const outStr = renderOutput(data, opts.format);
331
+
332
+ if (opts.output) {
333
+ try {
334
+ fs.writeFileSync(opts.output, outStr, "utf8");
335
+ } catch (e) {
336
+ console.error("Error: could not write --output file:", e.message || e);
337
+ process.exit(1);
338
+ }
339
+ } else {
340
+ process.stdout.write(outStr);
341
+ }
342
+ }
343
+
344
+ run().catch((err) => {
345
+ console.error("Extraction failed:", err.message || err);
346
+ process.exit(1);
347
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "extraktr",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Terminal client for Extraktr — calls the same POST /api/extract contract as the web app.",
6
+ "main": "index.js",
7
+ "type": "commonjs",
8
+ "license": "MIT",
9
+ "keywords": ["extraktr", "cli", "extraction"],
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "files": ["index.js", "README.md"],
14
+ "bin": {
15
+ "extraktr": "./index.js"
16
+ }
17
+ }