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.
- package/README.md +90 -0
- package/index.js +347 -0
- 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
|
+
}
|