conduyt 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 +50 -0
- package/dist/client.js +57 -0
- package/dist/config.js +57 -0
- package/dist/index.js +124 -0
- package/dist/output.js +16 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# conduyt
|
|
2
|
+
|
|
3
|
+
Command-line interface for [Conduyt CRM](https://conduyt.app) — manage contacts, deals, pipelines, and run AI insight queries from your terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g conduyt
|
|
9
|
+
# or run without installing
|
|
10
|
+
npx conduyt --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
Generate an API key in Conduyt under **Settings → API** (keys start with `cdy_`), then either save it:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
conduyt config set --key cdy_your_key_here
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or set environment variables (these override the saved config):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export CONDUYT_API_KEY=cdy_your_key_here
|
|
25
|
+
export CONDUYT_API_URL=https://conduyt.app # optional, defaults to https://conduyt.app
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The saved config lives at `~/.conduyt/config.json` and is written owner-only (`chmod 600`).
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
conduyt whoami # show the authenticated user
|
|
34
|
+
conduyt contacts list --limit 25 # list contacts (supports --cursor, --query)
|
|
35
|
+
conduyt contacts get <id> # fetch one contact
|
|
36
|
+
conduyt deals list --pipeline <id> # list deals
|
|
37
|
+
conduyt pipelines # list pipelines and stages
|
|
38
|
+
conduyt search "acme corp" # search across the CRM
|
|
39
|
+
conduyt insights summary # run an AI insight query
|
|
40
|
+
conduyt api GET /api/v1/companies # raw authenticated request (escape hatch)
|
|
41
|
+
conduyt config show # show resolved config (key masked)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pass `--input '{"...":"..."}'` to `insights` and `--body '{"...":"..."}'` to `api` to send a JSON payload.
|
|
45
|
+
|
|
46
|
+
All output is JSON, ready to pipe into `jq`.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { requireAuth } from "./config.js";
|
|
2
|
+
export class ConduytClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
apiKey;
|
|
5
|
+
constructor() {
|
|
6
|
+
const cfg = requireAuth();
|
|
7
|
+
this.baseUrl = cfg.apiUrl.replace(/\/+$/, "");
|
|
8
|
+
this.apiKey = cfg.apiKey;
|
|
9
|
+
}
|
|
10
|
+
async request(method, path, body) {
|
|
11
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
},
|
|
18
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
19
|
+
});
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
const json = text ? safeParse(text) : null;
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const msg = json?.error || res.statusText || `HTTP ${res.status}`;
|
|
24
|
+
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
return json;
|
|
27
|
+
}
|
|
28
|
+
get(path) {
|
|
29
|
+
return this.request("GET", path);
|
|
30
|
+
}
|
|
31
|
+
post(path, body) {
|
|
32
|
+
return this.request("POST", path, body);
|
|
33
|
+
}
|
|
34
|
+
patch(path, body) {
|
|
35
|
+
return this.request("PATCH", path, body);
|
|
36
|
+
}
|
|
37
|
+
del(path) {
|
|
38
|
+
return this.request("DELETE", path);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function safeParse(text) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function buildQuery(params) {
|
|
50
|
+
const usp = new URLSearchParams();
|
|
51
|
+
for (const [k, v] of Object.entries(params)) {
|
|
52
|
+
if (v !== undefined && v !== "")
|
|
53
|
+
usp.set(k, String(v));
|
|
54
|
+
}
|
|
55
|
+
const qs = usp.toString();
|
|
56
|
+
return qs ? `?${qs}` : "";
|
|
57
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".conduyt");
|
|
5
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
6
|
+
const DEFAULT_API_URL = "https://conduyt.app";
|
|
7
|
+
export function configPath() {
|
|
8
|
+
return CONFIG_PATH;
|
|
9
|
+
}
|
|
10
|
+
function readFile() {
|
|
11
|
+
if (!existsSync(CONFIG_PATH))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function saveConfig(update) {
|
|
21
|
+
const current = readFile();
|
|
22
|
+
const next = {
|
|
23
|
+
apiUrl: update.apiUrl ?? current.apiUrl ?? DEFAULT_API_URL,
|
|
24
|
+
apiKey: update.apiKey ?? current.apiKey ?? "",
|
|
25
|
+
};
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2));
|
|
28
|
+
// Config holds an API key — keep it owner-only.
|
|
29
|
+
try {
|
|
30
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// best effort on platforms without POSIX perms
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve config from environment first (CONDUYT_API_URL / CONDUYT_API_KEY),
|
|
39
|
+
* falling back to the saved config file.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveConfig() {
|
|
42
|
+
const file = readFile();
|
|
43
|
+
return {
|
|
44
|
+
apiUrl: process.env.CONDUYT_API_URL ?? file.apiUrl ?? DEFAULT_API_URL,
|
|
45
|
+
apiKey: process.env.CONDUYT_API_KEY ?? file.apiKey ?? "",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function requireAuth() {
|
|
49
|
+
const cfg = resolveConfig();
|
|
50
|
+
if (!cfg.apiKey) {
|
|
51
|
+
throw new Error("No API key configured. Run `conduyt config set --key cdy_...` or set CONDUYT_API_KEY.");
|
|
52
|
+
}
|
|
53
|
+
if (!cfg.apiKey.startsWith("cdy_")) {
|
|
54
|
+
throw new Error("CONDUYT_API_KEY must start with 'cdy_'. Generate one at Settings > API in Conduyt.");
|
|
55
|
+
}
|
|
56
|
+
return cfg;
|
|
57
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { ConduytClient, buildQuery } from "./client.js";
|
|
4
|
+
import { saveConfig, resolveConfig, configPath } from "./config.js";
|
|
5
|
+
import { print, fail } from "./output.js";
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name("conduyt")
|
|
9
|
+
.description("Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.")
|
|
10
|
+
.version("1.0.0");
|
|
11
|
+
// ---- config ----
|
|
12
|
+
const config = program.command("config").description("Manage CLI configuration");
|
|
13
|
+
config
|
|
14
|
+
.command("set")
|
|
15
|
+
.description("Save your API key and (optionally) API URL")
|
|
16
|
+
.option("--key <apiKey>", "API key (starts with cdy_)")
|
|
17
|
+
.option("--url <apiUrl>", "API base URL")
|
|
18
|
+
.action((opts) => {
|
|
19
|
+
if (!opts.key && !opts.url)
|
|
20
|
+
fail("Nothing to set. Pass --key and/or --url.");
|
|
21
|
+
const next = saveConfig({ apiKey: opts.key, apiUrl: opts.url });
|
|
22
|
+
process.stdout.write(`Saved to ${configPath()}\n`);
|
|
23
|
+
print({ apiUrl: next.apiUrl, apiKey: mask(next.apiKey) });
|
|
24
|
+
});
|
|
25
|
+
config
|
|
26
|
+
.command("show")
|
|
27
|
+
.description("Show the resolved configuration (API key masked)")
|
|
28
|
+
.action(() => {
|
|
29
|
+
const cfg = resolveConfig();
|
|
30
|
+
print({ apiUrl: cfg.apiUrl, apiKey: mask(cfg.apiKey), source: configPath() });
|
|
31
|
+
});
|
|
32
|
+
// ---- whoami ----
|
|
33
|
+
program
|
|
34
|
+
.command("whoami")
|
|
35
|
+
.description("Show the authenticated user")
|
|
36
|
+
.action(run(async (client) => client.get("/api/v1/users/me")));
|
|
37
|
+
// ---- contacts ----
|
|
38
|
+
const contacts = program.command("contacts").description("Manage contacts");
|
|
39
|
+
contacts
|
|
40
|
+
.command("list")
|
|
41
|
+
.description("List contacts")
|
|
42
|
+
.option("--limit <n>", "max results")
|
|
43
|
+
.option("--cursor <cursor>", "pagination cursor")
|
|
44
|
+
.option("--query <q>", "search query")
|
|
45
|
+
.action(run(async (client, opts) => client.get(`/api/v1/contacts${buildQuery({ limit: opts.limit, cursor: opts.cursor, query: opts.query })}`)));
|
|
46
|
+
contacts
|
|
47
|
+
.command("get <id>")
|
|
48
|
+
.description("Get a single contact by id")
|
|
49
|
+
.action(run(async (client, id) => client.get(`/api/v1/contacts/${encodeURIComponent(id)}`)));
|
|
50
|
+
// ---- deals ----
|
|
51
|
+
const deals = program.command("deals").description("Manage deals");
|
|
52
|
+
deals
|
|
53
|
+
.command("list")
|
|
54
|
+
.description("List deals")
|
|
55
|
+
.option("--limit <n>", "max results")
|
|
56
|
+
.option("--cursor <cursor>", "pagination cursor")
|
|
57
|
+
.option("--pipeline <id>", "filter by pipeline id")
|
|
58
|
+
.action(run(async (client, opts) => client.get(`/api/v1/deals${buildQuery({ limit: opts.limit, cursor: opts.cursor, pipelineId: opts.pipeline })}`)));
|
|
59
|
+
// ---- pipelines ----
|
|
60
|
+
program
|
|
61
|
+
.command("pipelines")
|
|
62
|
+
.description("List pipelines and their stages")
|
|
63
|
+
.action(run(async (client) => client.get("/api/v1/pipelines")));
|
|
64
|
+
// ---- search ----
|
|
65
|
+
program
|
|
66
|
+
.command("search <query>")
|
|
67
|
+
.description("Search across the CRM")
|
|
68
|
+
.option("--limit <n>", "max results")
|
|
69
|
+
.action(run(async (client, query, opts) => client.get(`/api/v1/search${buildQuery({ q: query, limit: opts.limit })}`)));
|
|
70
|
+
// ---- insights ----
|
|
71
|
+
program
|
|
72
|
+
.command("insights <type>")
|
|
73
|
+
.description("Run an AI insight query (e.g. summary, forecast)")
|
|
74
|
+
.option("--input <json>", "JSON payload to send with the request")
|
|
75
|
+
.action(run(async (client, type, opts) => {
|
|
76
|
+
let extra = {};
|
|
77
|
+
if (opts.input) {
|
|
78
|
+
try {
|
|
79
|
+
extra = JSON.parse(opts.input);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
fail("--input must be valid JSON");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return client.post("/api/v1/ai/insights", { type, ...extra });
|
|
86
|
+
}));
|
|
87
|
+
// ---- raw escape hatch ----
|
|
88
|
+
program
|
|
89
|
+
.command("api <method> <path>")
|
|
90
|
+
.description("Make a raw authenticated API request (escape hatch)")
|
|
91
|
+
.option("--body <json>", "JSON request body")
|
|
92
|
+
.action(run(async (client, method, path, opts) => {
|
|
93
|
+
let body;
|
|
94
|
+
if (opts.body) {
|
|
95
|
+
try {
|
|
96
|
+
body = JSON.parse(opts.body);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
fail("--body must be valid JSON");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return client.request(method.toUpperCase(), path, body);
|
|
103
|
+
}));
|
|
104
|
+
program.parseAsync(process.argv).catch(fail);
|
|
105
|
+
// helpers
|
|
106
|
+
function run(handler) {
|
|
107
|
+
return async (...args) => {
|
|
108
|
+
try {
|
|
109
|
+
const client = new ConduytClient();
|
|
110
|
+
const res = await handler(client, ...args);
|
|
111
|
+
print(res);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
fail(err);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function mask(key) {
|
|
119
|
+
if (!key)
|
|
120
|
+
return "(not set)";
|
|
121
|
+
if (key.length <= 12)
|
|
122
|
+
return "cdy_***";
|
|
123
|
+
return `${key.slice(0, 8)}…${key.slice(-4)}`;
|
|
124
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Unwrap the standard `{ data: ... }` API envelope when present. */
|
|
2
|
+
export function unwrap(res) {
|
|
3
|
+
if (res && typeof res === "object" && "data" in res) {
|
|
4
|
+
return res.data;
|
|
5
|
+
}
|
|
6
|
+
return res;
|
|
7
|
+
}
|
|
8
|
+
export function print(res) {
|
|
9
|
+
const data = unwrap(res);
|
|
10
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
11
|
+
}
|
|
12
|
+
export function fail(err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conduyt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"conduyt": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"start": "tsx src/index.ts",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"conduyt",
|
|
25
|
+
"crm",
|
|
26
|
+
"cli",
|
|
27
|
+
"sales",
|
|
28
|
+
"ai",
|
|
29
|
+
"api"
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://conduyt.app",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/ptaramona/conduyt-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/ptaramona/conduyt-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Conduyt",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"commander": "^12.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.5.0",
|
|
46
|
+
"tsx": "^4.19.0",
|
|
47
|
+
"@types/node": "^22.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|