@extraktor/cli 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 CHANGED
@@ -35,7 +35,7 @@ A long page comes in parts of about 24,000 characters. Each part gives the comma
35
35
  npx -y @extraktor/cli mcp add
36
36
  ```
37
37
 
38
- The command finds Claude Code, Codex, Cursor, VS Code, Gemini CLI and Windsurf on this computer and adds the server `https://extraktor.app/mcp` to each one. It keeps the other servers and settings, and a second run changes nothing. Each agent opens a sign-in page when it first uses Extraktor. The output shows the sign-in step for each agent.
38
+ The command finds Claude Code, Codex, Cursor, VS Code, Gemini CLI and Windsurf on this computer and adds the server `https://extraktor.app/mcp` to each one. In a terminal, it first asks which agents to change. When an agent or a script runs it, it asks nothing. It keeps the other servers and settings, and a second run changes nothing. Each agent opens a sign-in page when it first uses Extraktor. The output shows the sign-in step for each agent.
39
39
 
40
40
  Use `--agent <name>` to change only some agents, and `--json` for a JSON result. `extraktor mcp remove` removes the server. For the Claude app, open **Settings > Connectors > Add custom connector** and paste the server URL.
41
41
 
package/dist/cli.js CHANGED
@@ -7,5 +7,6 @@ const exitCode = await run(process.argv.slice(2), {
7
7
  env: process.env,
8
8
  readStdin: () => text(process.stdin),
9
9
  cwd: process.cwd(),
10
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI,
10
11
  });
11
12
  process.exitCode = exitCode;
package/dist/help.js CHANGED
@@ -192,6 +192,10 @@ server (https://extraktor.app/mcp) to each one. Each agent opens a sign-in
192
192
  page when it first uses Extraktor. A second run changes nothing. remove
193
193
  removes the server from each agent.
194
194
 
195
+ In a terminal, add and remove first ask which agents to change. When an
196
+ agent or a script runs the command, or with --agent or --json, the command
197
+ changes each agent that it finds and asks nothing.
198
+
195
199
  Agents: claude-code, codex, cursor, vscode, gemini-cli, windsurf. For the
196
200
  Claude app, open Settings > Connectors > Add custom connector and paste the
197
201
  server URL.
package/dist/install.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * at the same time. A second run changes nothing.
7
7
  */
8
8
  import { execFile } from "node:child_process";
9
- import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
9
+ import { access, constants, mkdir, readFile, rename, stat, writeFile, } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
11
  import path from "node:path";
12
12
  import { promisify } from "node:util";
@@ -80,9 +80,27 @@ const readText = async (file) => {
80
80
  throw error;
81
81
  }
82
82
  };
83
+ /** True when a program with this name is on the PATH. */
84
+ const onPath = async (program, { env, platform }) => {
85
+ const names = platform === "win32"
86
+ ? [`${program}.exe`, `${program}.cmd`, program]
87
+ : [program];
88
+ const files = (env.PATH ?? "")
89
+ .split(path.delimiter)
90
+ .filter(Boolean)
91
+ .flatMap((dir) => names.map((name) => path.join(dir, name)));
92
+ try {
93
+ await Promise.any(files.map((file) => access(file, constants.X_OK)));
94
+ return true;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ };
83
100
  const claudeCode = {
84
101
  id: "claude-code",
85
102
  name: "Claude Code",
103
+ detect: (context) => onPath("claude", context),
86
104
  next: "Run /mcp in Claude Code, select extraktor, and sign in.",
87
105
  add: async ({ exec, url }) => {
88
106
  const result = await exec("claude", [
@@ -153,6 +171,7 @@ const removeCodexTables = (toml) => {
153
171
  const codex = {
154
172
  id: "codex",
155
173
  name: "Codex",
174
+ detect: ({ env }) => exists(codexDir(env)),
156
175
  next: "Run: codex mcp login extraktor --scopes profile",
157
176
  add: async ({ env, url }) => {
158
177
  const dir = codexDir(env);
@@ -203,6 +222,7 @@ const jsonAgent = ({ dir, entry, file, key, ...agent }) => {
203
222
  const servers = (data) => jsonObjectSchema.safeParse(data[key]).data;
204
223
  return {
205
224
  ...agent,
225
+ detect: (context) => exists(dir(context)),
206
226
  add: async (context) => {
207
227
  const loaded = await load(context);
208
228
  if (!loaded) {
@@ -293,6 +313,11 @@ export const AGENTS = [
293
313
  export const AGENT_IDS = AGENTS.map(({ id }) => id);
294
314
  const AGENT_ID_SET = new Set(AGENT_IDS);
295
315
  export const isAgentId = (name) => AGENT_ID_SET.has(name);
316
+ /** The agents that are installed on this computer, in list order. */
317
+ export const findAgents = async (context) => {
318
+ const found = await Promise.all(AGENTS.map(async (agent) => (await agent.detect(context)) ? agent.id : null));
319
+ return new Set(found.filter((id) => id !== null));
320
+ };
296
321
  /** Adds or removes the server in each agent, all at the same time. */
297
322
  export const changeAgents = (action, ids, context) => Promise.all(AGENTS.filter(({ id }) => ids.has(id)).map(async (agent) => {
298
323
  const base = { id: agent.id, name: agent.name };
package/dist/pick.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The agent picker for `extraktor mcp add` and `mcp remove` in a terminal.
3
+ * Only a person sees it: the CLI loads it only when it runs in a terminal and
4
+ * no agent runs the CLI.
5
+ */
6
+ import { isCancel, multiselect } from "@clack/prompts";
7
+ export const pickAgents = async (action, agents) => {
8
+ const selected = await multiselect({
9
+ message: action === "add"
10
+ ? "Add Extraktor to which agents?"
11
+ : "Remove Extraktor from which agents?",
12
+ options: agents.map(({ found, id, name }) => ({
13
+ value: id,
14
+ label: name,
15
+ hint: found ? undefined : "not found on this computer",
16
+ disabled: !found,
17
+ })),
18
+ initialValues: agents.flatMap(({ found, id }) => (found ? [id] : [])),
19
+ required: true,
20
+ });
21
+ return isCancel(selected) ? null : new Set(selected);
22
+ };
package/dist/run.js CHANGED
@@ -388,12 +388,56 @@ const logout = async (args, io) => {
388
388
  };
389
389
  const MCP_ACTIONS = new Set(["add", "remove"]);
390
390
  const isMcpAction = (value) => MCP_ACTIONS.has(value);
391
+ /** The agents that --agent names, or all agents. Unknown names are usage errors. */
392
+ const namedAgents = async (names) => {
393
+ const { AGENT_IDS, isAgentId } = await import("./install.js");
394
+ const ids = new Set();
395
+ for (const name of names ?? AGENT_IDS) {
396
+ if (!isAgentId(name)) {
397
+ const match = closest(name, [...AGENT_IDS]);
398
+ throw usageError(`"${name}" is not an agent.${match ? ` Did you mean "--agent ${match}"?` : ""} Agents: ${AGENT_IDS.join(", ")}.`, "mcp");
399
+ }
400
+ ids.add(name);
401
+ }
402
+ return ids;
403
+ };
404
+ /**
405
+ * Asks which agents to change, with the found agents selected. Returns null
406
+ * when the person cancels, and all agents when none is found, so that the
407
+ * caller reports that.
408
+ */
409
+ const pickAgents = async (action, context, io) => {
410
+ const { AGENTS, findAgents } = await import("./install.js");
411
+ const found = await findAgents(context);
412
+ if (found.size === 0) {
413
+ return new Set(AGENTS.map(({ id }) => id));
414
+ }
415
+ let picker = io.pickAgents;
416
+ if (!picker) {
417
+ // The prompt library loads only here, for a person in a terminal.
418
+ const module = await import("./pick.js");
419
+ picker = module.pickAgents;
420
+ }
421
+ return picker(action, AGENTS.map(({ id, name }) => ({ id, name, found: found.has(id) })));
422
+ };
423
+ /** Prints the result. Throws when no agent was found or a change failed. */
424
+ const reportAgents = async (action, results, url, json, io) => {
425
+ const found = results.filter(({ status }) => status !== "not-found");
426
+ if (found.length === 0 && action === "add") {
427
+ throw new CliError("No coding agent was found on this computer.", EXIT.failure, `Install Claude Code, Codex, Cursor, VS Code, Gemini CLI or Windsurf, then run this command again. Or add ${url} to your MCP client by hand.`, "NO_AGENT_FOUND");
428
+ }
429
+ const { formatResults } = await import("./install.js");
430
+ io.stdout(json
431
+ ? `${JSON.stringify({ action, url, agents: results }, null, 2)}\n`
432
+ : formatResults(action, results, url));
433
+ const failed = results.filter(({ status }) => status === "failed").length;
434
+ if (failed > 0) {
435
+ throw new CliError(`${failed} of ${found.length} agents failed. The output tells why.`, EXIT.failure, undefined, SOME_AGENTS_FAILED);
436
+ }
437
+ };
391
438
  const mcp = async (args, io) => {
392
439
  const [action = "", ...rest] = args;
393
- if (action === "" ||
394
- action === "help" ||
395
- action === "--help" ||
396
- action === "-h") {
440
+ if (["", "help", "--help", "-h"].includes(action)) {
397
441
  io.stdout(MCP_HELP);
398
442
  return;
399
443
  }
@@ -408,33 +452,34 @@ const mcp = async (args, io) => {
408
452
  if (positionals.length > 0) {
409
453
  throw usageError(`mcp ${action} does not accept arguments.`, "mcp");
410
454
  }
411
- const { AGENT_IDS, changeAgents, defaultExec, formatResults, isAgentId } = await import("./install.js");
412
- const ids = new Set();
413
- for (const name of values.agent ?? AGENT_IDS) {
414
- if (!isAgentId(name)) {
415
- const match = closest(name, [...AGENT_IDS]);
416
- throw usageError(`"${name}" is not an agent.${match ? ` Did you mean "--agent ${match}"?` : ""} Agents: ${AGENT_IDS.join(", ")}.`, "mcp");
417
- }
418
- ids.add(name);
419
- }
455
+ const [named, { changeAgents, defaultExec }] = await Promise.all([
456
+ namedAgents(values.agent),
457
+ import("./install.js"),
458
+ ]);
459
+ let ids = named;
420
460
  const url = `${baseUrlOf(io.env)}/mcp`;
421
- const results = await changeAgents(action, ids, {
461
+ const context = {
422
462
  env: io.env,
423
463
  exec: io.exec ?? defaultExec,
424
464
  platform: io.platform ?? process.platform,
425
465
  url,
426
- });
427
- const found = results.filter(({ status }) => status !== "not-found");
428
- if (found.length === 0 && action === "add") {
429
- throw new CliError("No coding agent was found on this computer.", EXIT.failure, `Install Claude Code, Codex, Cursor, VS Code, Gemini CLI or Windsurf, then run this command again. Or add ${url} to your MCP client by hand.`, "NO_AGENT_FOUND");
430
- }
431
- io.stdout(values.json
432
- ? `${JSON.stringify({ action, url, agents: results }, null, 2)}\n`
433
- : formatResults(action, results, url));
434
- const failed = results.filter(({ status }) => status === "failed").length;
435
- if (failed > 0) {
436
- throw new CliError(`${failed} of ${found.length} agents failed. The output tells why.`, EXIT.failure, undefined, SOME_AGENTS_FAILED);
466
+ };
467
+ // A person in a terminal picks the agents. Agents, scripts, --agent and
468
+ // --json change each agent that is found, with no question.
469
+ const ask = io.interactive === true &&
470
+ values.agent === undefined &&
471
+ !values.json &&
472
+ detectAgent(io.env) === null;
473
+ if (ask) {
474
+ const picked = await pickAgents(action, context, io);
475
+ if (picked === null) {
476
+ io.stderr("Canceled. Nothing changed.\n");
477
+ return;
478
+ }
479
+ ids = picked;
437
480
  }
481
+ const results = await changeAgents(action, ids, context);
482
+ await reportAgents(action, results, url, values.json === true, io);
438
483
  };
439
484
  const COMMANDS = { extract, search, login, logout, mcp };
440
485
  /** Names that agents guess for mcp add. They run it, with no error. */
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Keep equal to package.json. A test checks it. */
2
- export const VERSION = "0.1.1";
2
+ export const VERSION = "0.1.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extraktor/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Read live web pages as Markdown and search the web from the terminal. Built for AI agents.",
5
5
  "keywords": [
6
6
  "agent",
@@ -36,6 +36,7 @@
36
36
  "prepack": "npm run build"
37
37
  },
38
38
  "dependencies": {
39
+ "@clack/prompts": "^1.8.1",
39
40
  "zod": "^4.6.5"
40
41
  },
41
42
  "devDependencies": {