@crmy/cli 0.5.2 → 0.5.4

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/dist/index.js CHANGED
@@ -1 +1,2040 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command27 } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { Command } from "commander";
8
+ import crypto from "crypto";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ function initCommand() {
12
+ return new Command("init").description("Initialize crmy.ai: configure database, run migrations, create user").action(async () => {
13
+ const { default: inquirer } = await import("inquirer");
14
+ console.log("\n crmy.ai \u2014 Agent-first CRM setup\n");
15
+ const answers = await inquirer.prompt([
16
+ {
17
+ type: "input",
18
+ name: "databaseUrl",
19
+ message: "PostgreSQL URL?",
20
+ default: "postgresql://localhost:5432/crmy"
21
+ },
22
+ {
23
+ type: "input",
24
+ name: "name",
25
+ message: "Your name?"
26
+ },
27
+ {
28
+ type: "input",
29
+ name: "email",
30
+ message: "Your email?"
31
+ },
32
+ {
33
+ type: "password",
34
+ name: "password",
35
+ message: "Password?",
36
+ mask: "*"
37
+ }
38
+ ]);
39
+ process.env.CRMY_IMPORTED = "1";
40
+ try {
41
+ const { initPool, closePool } = await import("@crmy/server");
42
+ const { runMigrations } = await import("@crmy/server");
43
+ console.log("\nConnecting to database...");
44
+ const db = await initPool(answers.databaseUrl);
45
+ console.log("Connected.");
46
+ console.log("Running migrations...");
47
+ const ran = await runMigrations(db);
48
+ if (ran.length > 0) {
49
+ console.log(` Ran ${ran.length} migration(s): ${ran.join(", ")}`);
50
+ } else {
51
+ console.log(" No pending migrations.");
52
+ }
53
+ const tenantResult = await db.query(
54
+ `INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
55
+ ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
56
+ RETURNING id`
57
+ );
58
+ const tenantId = tenantResult.rows[0].id;
59
+ const passwordHash = crypto.createHash("sha256").update(answers.password).digest("hex");
60
+ const userResult = await db.query(
61
+ `INSERT INTO users (tenant_id, email, name, role, password_hash)
62
+ VALUES ($1, $2, $3, 'owner', $4)
63
+ ON CONFLICT (tenant_id, email) DO UPDATE SET name = $3, password_hash = $4
64
+ RETURNING id`,
65
+ [tenantId, answers.email, answers.name, passwordHash]
66
+ );
67
+ const userId = userResult.rows[0].id;
68
+ const rawKey = "crmy_" + crypto.randomBytes(32).toString("hex");
69
+ const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
70
+ await db.query(
71
+ `INSERT INTO api_keys (tenant_id, user_id, key_hash, label, scopes)
72
+ VALUES ($1, $2, $3, 'default', '{read,write,admin}')`,
73
+ [tenantId, userId, keyHash]
74
+ );
75
+ const jwtSecret = crypto.randomBytes(32).toString("hex");
76
+ const config = {
77
+ serverUrl: "http://localhost:3000",
78
+ apiKey: rawKey,
79
+ tenantId: "default",
80
+ database: {
81
+ url: answers.databaseUrl
82
+ },
83
+ jwtSecret,
84
+ hitl: {
85
+ requireApproval: ["bulk_update", "bulk_delete", "send_email"],
86
+ autoApproveSeconds: 0
87
+ }
88
+ };
89
+ const configPath = path.join(process.cwd(), ".crmy.json");
90
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
91
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
92
+ const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
93
+ if (!gitignoreContent.includes(".crmy.json")) {
94
+ fs.appendFileSync(gitignorePath, "\n.crmy.json\n");
95
+ }
96
+ await closePool();
97
+ console.log("\n \u2713 crmy.ai initialized\n");
98
+ console.log(" Add to Claude Code:");
99
+ console.log(" claude mcp add crmy -- npx crmy mcp\n");
100
+ console.log(" Or start the server:");
101
+ console.log(" npx crmy server\n");
102
+ } catch (err) {
103
+ console.error("\nSetup failed:", err instanceof Error ? err.message : err);
104
+ process.exit(1);
105
+ }
106
+ });
107
+ }
108
+
109
+ // src/commands/server.ts
110
+ import { Command as Command2 } from "commander";
111
+
112
+ // src/config.ts
113
+ import fs2 from "fs";
114
+ import path2 from "path";
115
+ import os from "os";
116
+ var AUTH_DIR = path2.join(os.homedir(), ".crmy");
117
+ var AUTH_FILE = path2.join(AUTH_DIR, "auth.json");
118
+ function loadConfigFile() {
119
+ const configPath = path2.join(process.cwd(), ".crmy.json");
120
+ try {
121
+ const raw = fs2.readFileSync(configPath, "utf-8");
122
+ return JSON.parse(raw);
123
+ } catch {
124
+ return {};
125
+ }
126
+ }
127
+ function loadAuthState() {
128
+ try {
129
+ const raw = fs2.readFileSync(AUTH_FILE, "utf-8");
130
+ const state = JSON.parse(raw);
131
+ if (state.expiresAt && new Date(state.expiresAt) < /* @__PURE__ */ new Date()) {
132
+ return null;
133
+ }
134
+ return state;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function saveAuthState(state) {
140
+ fs2.mkdirSync(AUTH_DIR, { recursive: true });
141
+ fs2.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
142
+ }
143
+ function clearAuthState() {
144
+ try {
145
+ fs2.unlinkSync(AUTH_FILE);
146
+ } catch {
147
+ }
148
+ }
149
+ function resolveServerUrl() {
150
+ return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
151
+ }
152
+
153
+ // src/commands/server.ts
154
+ function serverCommand() {
155
+ return new Command2("server").description("Start the crmy HTTP server").option("--port <port>", "HTTP port", "3000").action(async (opts) => {
156
+ const config = loadConfigFile();
157
+ process.env.DATABASE_URL = config.database?.url ?? process.env.DATABASE_URL;
158
+ process.env.JWT_SECRET = config.jwtSecret ?? process.env.JWT_SECRET ?? "dev-secret";
159
+ process.env.PORT = opts.port;
160
+ process.env.CRMY_IMPORTED = "1";
161
+ if (!process.env.DATABASE_URL) {
162
+ console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
163
+ process.exit(1);
164
+ }
165
+ const { createApp, loadConfig } = await import("@crmy/server");
166
+ const serverConfig = loadConfig();
167
+ const { app } = await createApp(serverConfig);
168
+ app.listen(serverConfig.port, () => {
169
+ console.log(`crmy server ready on :${serverConfig.port}`);
170
+ });
171
+ });
172
+ }
173
+
174
+ // src/commands/mcp.ts
175
+ import { Command as Command3 } from "commander";
176
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
177
+ function mcpCommand() {
178
+ return new Command3("mcp").description("Start stdio MCP server (for Claude Code)").action(async () => {
179
+ const config = loadConfigFile();
180
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
181
+ const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
182
+ if (!databaseUrl) {
183
+ console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
184
+ process.exit(1);
185
+ }
186
+ process.env.CRMY_IMPORTED = "1";
187
+ const { initPool, createMcpServer } = await import("@crmy/server");
188
+ const { runMigrations } = await import("@crmy/server");
189
+ const db = await initPool(databaseUrl);
190
+ await runMigrations(db);
191
+ let actor = {
192
+ tenant_id: "",
193
+ actor_id: "cli-agent",
194
+ actor_type: "agent",
195
+ role: "owner"
196
+ };
197
+ if (apiKey) {
198
+ const crypto2 = await import("crypto");
199
+ const keyHash = crypto2.createHash("sha256").update(apiKey).digest("hex");
200
+ const result = await db.query(
201
+ `SELECT ak.tenant_id, ak.user_id, ak.scopes, u.role
202
+ FROM api_keys ak LEFT JOIN users u ON ak.user_id = u.id
203
+ WHERE ak.key_hash = $1`,
204
+ [keyHash]
205
+ );
206
+ if (result.rows.length > 0) {
207
+ const row = result.rows[0];
208
+ actor = {
209
+ tenant_id: row.tenant_id,
210
+ actor_id: row.user_id ?? "api-key-agent",
211
+ actor_type: "agent",
212
+ role: row.role ?? "member"
213
+ };
214
+ }
215
+ }
216
+ if (!actor.tenant_id) {
217
+ const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
218
+ if (tenantResult.rows.length > 0) {
219
+ actor.tenant_id = tenantResult.rows[0].id;
220
+ }
221
+ }
222
+ const server = createMcpServer(db, () => actor);
223
+ const transport = new StdioServerTransport();
224
+ await server.connect(transport);
225
+ });
226
+ }
227
+
228
+ // src/commands/contacts.ts
229
+ import { Command as Command4 } from "commander";
230
+
231
+ // src/client.ts
232
+ var TOOL_REST_MAP = {
233
+ // Contacts
234
+ contact_create: { method: "POST", path: () => "/api/v1/contacts" },
235
+ contact_get: { method: "GET", path: (i) => `/api/v1/contacts/${i.id}` },
236
+ contact_search: { method: "GET", path: (i) => `/api/v1/contacts?q=${encodeURIComponent(i.query ?? "")}&limit=${i.limit ?? 20}${i.lifecycle_stage ? `&stage=${i.lifecycle_stage}` : ""}${i.cursor ? `&cursor=${i.cursor}` : ""}` },
237
+ contact_update: { method: "PATCH", path: (i) => `/api/v1/contacts/${i.id}` },
238
+ contact_set_lifecycle: { method: "PATCH", path: (i) => `/api/v1/contacts/${i.id}` },
239
+ contact_log_activity: { method: "POST", path: () => "/api/v1/activities" },
240
+ contact_get_timeline: { method: "GET", path: (i) => `/api/v1/contacts/${i.id}/timeline` },
241
+ // Accounts
242
+ account_create: { method: "POST", path: () => "/api/v1/accounts" },
243
+ account_get: { method: "GET", path: (i) => `/api/v1/accounts/${i.id}` },
244
+ account_search: { method: "GET", path: (i) => `/api/v1/accounts?q=${encodeURIComponent(i.query ?? "")}&limit=${i.limit ?? 20}` },
245
+ account_update: { method: "PATCH", path: (i) => `/api/v1/accounts/${i.id}` },
246
+ // Opportunities
247
+ opportunity_create: { method: "POST", path: () => "/api/v1/opportunities" },
248
+ opportunity_get: { method: "GET", path: (i) => `/api/v1/opportunities/${i.id}` },
249
+ opportunity_search: { method: "GET", path: (i) => `/api/v1/opportunities?q=${encodeURIComponent(i.query ?? "")}&limit=${i.limit ?? 20}${i.stage ? `&stage=${i.stage}` : ""}` },
250
+ opportunity_advance_stage: { method: "PATCH", path: (i) => `/api/v1/opportunities/${i.id}` },
251
+ opportunity_update: { method: "PATCH", path: (i) => `/api/v1/opportunities/${i.id}` },
252
+ // Activities
253
+ activity_create: { method: "POST", path: () => "/api/v1/activities" },
254
+ activity_get: { method: "GET", path: (i) => `/api/v1/activities?limit=${i.limit ?? 20}` },
255
+ activity_search: { method: "GET", path: (i) => `/api/v1/activities?limit=${i.limit ?? 20}` },
256
+ // Use Cases
257
+ use_case_create: { method: "POST", path: () => "/api/v1/use-cases" },
258
+ use_case_get: { method: "GET", path: (i) => `/api/v1/use-cases/${i.id}` },
259
+ use_case_search: { method: "GET", path: (i) => `/api/v1/use-cases?q=${encodeURIComponent(i.query ?? "")}&limit=${i.limit ?? 20}${i.stage ? `&stage=${i.stage}` : ""}${i.account_id ? `&account_id=${i.account_id}` : ""}` },
260
+ use_case_update: { method: "PATCH", path: (i) => `/api/v1/use-cases/${i.id}` },
261
+ use_case_delete: { method: "DELETE", path: (i) => `/api/v1/use-cases/${i.id}` },
262
+ use_case_advance_stage: { method: "POST", path: (i) => `/api/v1/use-cases/${i.id}/stage` },
263
+ use_case_update_consumption: { method: "POST", path: (i) => `/api/v1/use-cases/${i.id}/consumption` },
264
+ use_case_set_health: { method: "POST", path: (i) => `/api/v1/use-cases/${i.id}/health` },
265
+ use_case_link_contact: { method: "POST", path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts` },
266
+ use_case_unlink_contact: { method: "DELETE", path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts/${i.contact_id}` },
267
+ use_case_list_contacts: { method: "GET", path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts` },
268
+ use_case_get_timeline: { method: "GET", path: (i) => `/api/v1/use-cases/${i.id}/timeline` },
269
+ use_case_summary: { method: "GET", path: (i) => `/api/v1/analytics/use-cases?group_by=${i.group_by ?? "stage"}` },
270
+ // Analytics
271
+ pipeline_summary: { method: "GET", path: (i) => `/api/v1/analytics/pipeline${i.owner_id ? `?owner_id=${i.owner_id}` : ""}` },
272
+ pipeline_forecast: { method: "GET", path: (i) => `/api/v1/analytics/forecast${i.period ? `?period=${i.period}` : ""}` },
273
+ // HITL
274
+ hitl_list_pending: { method: "GET", path: () => "/api/v1/hitl" },
275
+ hitl_check_status: { method: "GET", path: (i) => `/api/v1/hitl/${i.id}` },
276
+ hitl_submit_request: { method: "POST", path: () => "/api/v1/hitl" },
277
+ hitl_resolve: { method: "POST", path: (i) => `/api/v1/hitl/${i.id}/resolve` },
278
+ // Webhooks
279
+ webhook_create: { method: "POST", path: () => "/api/v1/webhooks" },
280
+ webhook_list: { method: "GET", path: () => "/api/v1/webhooks" },
281
+ webhook_get: { method: "GET", path: (i) => `/api/v1/webhooks/${i.id}` },
282
+ webhook_update: { method: "PATCH", path: (i) => `/api/v1/webhooks/${i.id}` },
283
+ webhook_delete: { method: "DELETE", path: (i) => `/api/v1/webhooks/${i.id}` },
284
+ webhook_list_deliveries: { method: "GET", path: (i) => `/api/v1/webhooks/${i.endpoint_id ?? i.id}/deliveries` },
285
+ // Emails
286
+ email_create: { method: "POST", path: () => "/api/v1/emails" },
287
+ email_get: { method: "GET", path: (i) => `/api/v1/emails/${i.id}` },
288
+ email_search: { method: "GET", path: (i) => `/api/v1/emails?limit=${i.limit ?? 20}` },
289
+ // Custom Fields
290
+ custom_field_create: { method: "POST", path: () => "/api/v1/custom-fields" },
291
+ custom_field_list: { method: "GET", path: (i) => `/api/v1/custom-fields?object_type=${i.object_type}` },
292
+ custom_field_delete: { method: "DELETE", path: (i) => `/api/v1/custom-fields/${i.id}` },
293
+ // Notes
294
+ note_create: { method: "POST", path: () => "/api/v1/notes" },
295
+ note_get: { method: "GET", path: (i) => `/api/v1/notes/${i.id}` },
296
+ note_list: { method: "GET", path: (i) => `/api/v1/notes?object_type=${i.object_type}&object_id=${i.object_id}` },
297
+ note_delete: { method: "DELETE", path: (i) => `/api/v1/notes/${i.id}` },
298
+ // Workflows
299
+ workflow_create: { method: "POST", path: () => "/api/v1/workflows" },
300
+ workflow_get: { method: "GET", path: (i) => `/api/v1/workflows/${i.id}` },
301
+ workflow_list: { method: "GET", path: () => "/api/v1/workflows" },
302
+ workflow_delete: { method: "DELETE", path: (i) => `/api/v1/workflows/${i.id}` },
303
+ workflow_run_list: { method: "GET", path: (i) => `/api/v1/workflows/${i.workflow_id ?? i.id}/runs` },
304
+ // Events
305
+ event_search: { method: "GET", path: (i) => `/api/v1/events?${i.object_id ? `object_id=${i.object_id}&` : ""}limit=${i.limit ?? 20}` },
306
+ // Search
307
+ search: { method: "GET", path: (i) => `/api/v1/search?q=${encodeURIComponent(i.query ?? "")}` },
308
+ // Meta
309
+ schema_get: { method: "GET", path: () => "/health" },
310
+ tenant_get_stats: { method: "GET", path: () => "/health" },
311
+ // Actors
312
+ actor_register: { method: "POST", path: () => "/api/v1/actors" },
313
+ actor_get: { method: "GET", path: (i) => `/api/v1/actors/${i.id}` },
314
+ actor_list: { method: "GET", path: (i) => `/api/v1/actors?limit=${i.limit ?? 20}${i.actor_type ? `&actor_type=${i.actor_type}` : ""}${i.query ? `&q=${encodeURIComponent(i.query)}` : ""}` },
315
+ actor_update: { method: "PATCH", path: (i) => `/api/v1/actors/${i.id}` },
316
+ actor_whoami: { method: "GET", path: () => "/api/v1/actors/whoami" },
317
+ // Assignments
318
+ assignment_create: { method: "POST", path: () => "/api/v1/assignments" },
319
+ assignment_get: { method: "GET", path: (i) => `/api/v1/assignments/${i.id}` },
320
+ assignment_list: { method: "GET", path: (i) => `/api/v1/assignments?limit=${i.limit ?? 20}${i.assigned_to ? `&assigned_to=${i.assigned_to}` : ""}${i.assigned_by ? `&assigned_by=${i.assigned_by}` : ""}${i.status ? `&status=${i.status}` : ""}` },
321
+ assignment_update: { method: "PATCH", path: (i) => `/api/v1/assignments/${i.id}` },
322
+ assignment_accept: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/accept` },
323
+ assignment_complete: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/complete` },
324
+ assignment_decline: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/decline` },
325
+ // Context Entries
326
+ context_add: { method: "POST", path: () => "/api/v1/context" },
327
+ context_get: { method: "GET", path: (i) => `/api/v1/context/${i.id}` },
328
+ context_list: { method: "GET", path: (i) => `/api/v1/context?limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ""}${i.subject_id ? `&subject_id=${i.subject_id}` : ""}${i.context_type ? `&context_type=${i.context_type}` : ""}` },
329
+ context_supersede: { method: "POST", path: (i) => `/api/v1/context/${i.id}/supersede` },
330
+ context_search: { method: "GET", path: (i) => `/api/v1/context/search?q=${encodeURIComponent(i.query)}&limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ""}${i.context_type ? `&context_type=${i.context_type}` : ""}${i.tag ? `&tag=${i.tag}` : ""}${i.current_only === false ? "&current_only=false" : ""}` },
331
+ context_review: { method: "POST", path: (i) => `/api/v1/context/${i.id}/review` },
332
+ context_stale: { method: "GET", path: (i) => `/api/v1/context/stale?limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ""}${i.subject_id ? `&subject_id=${i.subject_id}` : ""}` },
333
+ // Activity Type Registry
334
+ activity_type_list: { method: "GET", path: (i) => `/api/v1/activity-types${i.category ? `?category=${i.category}` : ""}` },
335
+ activity_type_add: { method: "POST", path: () => "/api/v1/activity-types" },
336
+ activity_type_remove: { method: "DELETE", path: (i) => `/api/v1/activity-types/${i.type_name}` },
337
+ // Context Type Registry
338
+ context_type_list: { method: "GET", path: () => "/api/v1/context-types" },
339
+ context_type_add: { method: "POST", path: () => "/api/v1/context-types" },
340
+ context_type_remove: { method: "DELETE", path: (i) => `/api/v1/context-types/${i.type_name}` },
341
+ // Briefing
342
+ briefing_get: { method: "GET", path: (i) => `/api/v1/briefing/${i.subject_type}/${i.subject_id}?format=${i.format ?? "json"}${i.since ? `&since=${i.since}` : ""}${i.include_stale ? "&include_stale=true" : ""}${i.context_types ? `&context_types=${i.context_types.join(",")}` : ""}` },
343
+ // Assignment: Start, Block, Cancel
344
+ assignment_start: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/start` },
345
+ assignment_block: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/block` },
346
+ assignment_cancel: { method: "POST", path: (i) => `/api/v1/assignments/${i.id}/cancel` },
347
+ // Activity Timeline (enhanced)
348
+ activity_get_timeline: { method: "GET", path: (i) => `/api/v1/activities?subject_type=${i.subject_type}&subject_id=${i.subject_id}&limit=${i.limit ?? 50}` }
349
+ };
350
+ function createHttpClient(serverUrl, token) {
351
+ return {
352
+ async call(toolName, input) {
353
+ const mapping = TOOL_REST_MAP[toolName];
354
+ if (!mapping) {
355
+ throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
356
+ }
357
+ const { method, path: path3 } = mapping;
358
+ const url = `${serverUrl.replace(/\/$/, "")}${path3(input)}`;
359
+ const headers = {
360
+ "Authorization": `Bearer ${token}`,
361
+ "Content-Type": "application/json"
362
+ };
363
+ const fetchOpts = { method, headers };
364
+ if (method === "POST" || method === "PATCH" || method === "PUT") {
365
+ const body = { ...input };
366
+ delete body.id;
367
+ fetchOpts.body = JSON.stringify(body);
368
+ }
369
+ const res = await fetch(url, fetchOpts);
370
+ if (res.status === 401) {
371
+ throw new Error("Authentication expired. Run `crmy login` to re-authenticate.");
372
+ }
373
+ const responseBody = await res.text();
374
+ if (!res.ok) {
375
+ let detail = responseBody;
376
+ try {
377
+ detail = JSON.parse(responseBody).detail ?? responseBody;
378
+ } catch {
379
+ }
380
+ throw new Error(`API error (${res.status}): ${detail}`);
381
+ }
382
+ return responseBody;
383
+ },
384
+ async close() {
385
+ }
386
+ };
387
+ }
388
+ async function createDbClient(databaseUrl, apiKey) {
389
+ process.env.CRMY_IMPORTED = "1";
390
+ const { initPool, closePool, getAllTools } = await import("@crmy/server");
391
+ const db = await initPool(databaseUrl);
392
+ let actor = {
393
+ tenant_id: "",
394
+ actor_id: "cli-user",
395
+ actor_type: "user",
396
+ role: "owner"
397
+ };
398
+ if (apiKey) {
399
+ const crypto2 = await import("crypto");
400
+ const keyHash = crypto2.createHash("sha256").update(apiKey).digest("hex");
401
+ const result = await db.query(
402
+ `SELECT ak.tenant_id, ak.user_id, u.role
403
+ FROM api_keys ak LEFT JOIN users u ON ak.user_id = u.id
404
+ WHERE ak.key_hash = $1`,
405
+ [keyHash]
406
+ );
407
+ if (result.rows.length > 0) {
408
+ actor.tenant_id = result.rows[0].tenant_id;
409
+ actor.actor_id = result.rows[0].user_id ?? "cli-user";
410
+ actor.role = result.rows[0].role ?? "owner";
411
+ }
412
+ }
413
+ if (!actor.tenant_id) {
414
+ const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
415
+ if (tenantResult.rows.length > 0) {
416
+ actor.tenant_id = tenantResult.rows[0].id;
417
+ }
418
+ }
419
+ const tools = getAllTools(db);
420
+ return {
421
+ async call(toolName, input) {
422
+ const tool = tools.find((t) => t.name === toolName);
423
+ if (!tool) throw new Error(`Unknown tool: ${toolName}`);
424
+ const result = await tool.handler(input, actor);
425
+ return JSON.stringify(result, null, 2);
426
+ },
427
+ async close() {
428
+ await closePool();
429
+ }
430
+ };
431
+ }
432
+ async function getClient() {
433
+ const config = loadConfigFile();
434
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
435
+ if (databaseUrl) {
436
+ const apiKey2 = process.env.CRMY_API_KEY ?? config.apiKey;
437
+ return createDbClient(databaseUrl, apiKey2);
438
+ }
439
+ const auth = loadAuthState();
440
+ if (auth) {
441
+ return createHttpClient(auth.serverUrl, auth.token);
442
+ }
443
+ const serverUrl = process.env.CRMY_SERVER_URL ?? config.serverUrl;
444
+ const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
445
+ if (serverUrl && apiKey) {
446
+ return createHttpClient(serverUrl, apiKey);
447
+ }
448
+ console.error("Not connected. Run `crmy auth setup` and `crmy login`, or `crmy init` for local mode.");
449
+ process.exit(1);
450
+ }
451
+
452
+ // src/commands/contacts.ts
453
+ function contactsCommand() {
454
+ const cmd = new Command4("contacts").description("Manage contacts");
455
+ cmd.command("list").option("-q, --query <query>", "Search query").option("--stage <stage>", "Filter by lifecycle stage").action(async (opts) => {
456
+ const client = await getClient();
457
+ const result = await client.call("contact_search", {
458
+ query: opts.query,
459
+ lifecycle_stage: opts.stage,
460
+ limit: 20
461
+ });
462
+ const data = JSON.parse(result);
463
+ if (data.contacts?.length === 0) {
464
+ console.log("No contacts found.");
465
+ return;
466
+ }
467
+ console.table(data.contacts?.map((c) => ({
468
+ id: c.id.slice(0, 8),
469
+ name: `${c.first_name} ${c.last_name}`,
470
+ email: c.email ?? "",
471
+ stage: c.lifecycle_stage,
472
+ company: c.company_name ?? ""
473
+ })));
474
+ if (data.total > 20) console.log(`
475
+ Showing 20 of ${data.total} contacts`);
476
+ await client.close();
477
+ });
478
+ cmd.command("get <id>").action(async (id) => {
479
+ const client = await getClient();
480
+ const result = await client.call("contact_get", { id });
481
+ console.log(JSON.parse(result));
482
+ await client.close();
483
+ });
484
+ cmd.command("create").action(async () => {
485
+ const { default: inquirer } = await import("inquirer");
486
+ const answers = await inquirer.prompt([
487
+ { type: "input", name: "first_name", message: "First name:" },
488
+ { type: "input", name: "last_name", message: "Last name:" },
489
+ { type: "input", name: "email", message: "Email:" },
490
+ { type: "input", name: "company_name", message: "Company:" }
491
+ ]);
492
+ const client = await getClient();
493
+ const result = await client.call("contact_create", {
494
+ first_name: answers.first_name,
495
+ last_name: answers.last_name || void 0,
496
+ email: answers.email || void 0,
497
+ company_name: answers.company_name || void 0
498
+ });
499
+ const data = JSON.parse(result);
500
+ console.log(`
501
+ Created contact: ${data.contact.id}
502
+ `);
503
+ await client.close();
504
+ });
505
+ cmd.command("delete <id>").description("Permanently delete a contact (admin/owner only)").action(async (id) => {
506
+ const { default: inquirer } = await import("inquirer");
507
+ const { confirm } = await inquirer.prompt([
508
+ { type: "confirm", name: "confirm", message: `Delete contact ${id}? This cannot be undone.`, default: false }
509
+ ]);
510
+ if (!confirm) {
511
+ console.log(" Cancelled.");
512
+ return;
513
+ }
514
+ const client = await getClient();
515
+ const result = await client.call("contact_delete", { id });
516
+ const data = JSON.parse(result);
517
+ if (data.deleted) console.log(` Deleted.`);
518
+ await client.close();
519
+ });
520
+ return cmd;
521
+ }
522
+
523
+ // src/commands/accounts.ts
524
+ import { Command as Command5 } from "commander";
525
+ function accountsCommand() {
526
+ const cmd = new Command5("accounts").description("Manage accounts");
527
+ cmd.command("list").option("-q, --query <query>", "Search query").action(async (opts) => {
528
+ const client = await getClient();
529
+ const result = await client.call("account_search", { query: opts.query, limit: 20 });
530
+ const data = JSON.parse(result);
531
+ if (data.accounts?.length === 0) {
532
+ console.log("No accounts found.");
533
+ return;
534
+ }
535
+ console.table(data.accounts?.map((a) => ({
536
+ id: a.id.slice(0, 8),
537
+ name: a.name,
538
+ industry: a.industry ?? "",
539
+ health: a.health_score ?? ""
540
+ })));
541
+ await client.close();
542
+ });
543
+ cmd.command("get <id>").description("Get account details including contacts and open opportunities").action(async (id) => {
544
+ const client = await getClient();
545
+ const result = await client.call("account_get", { id });
546
+ console.log(JSON.parse(result));
547
+ await client.close();
548
+ });
549
+ cmd.command("create").description("Create a new account").action(async () => {
550
+ const { default: inquirer } = await import("inquirer");
551
+ const answers = await inquirer.prompt([
552
+ { type: "input", name: "name", message: "Account name:" },
553
+ { type: "input", name: "domain", message: "Domain (optional):" },
554
+ { type: "input", name: "industry", message: "Industry (optional):" },
555
+ { type: "input", name: "website", message: "Website (optional):" }
556
+ ]);
557
+ const client = await getClient();
558
+ const result = await client.call("account_create", {
559
+ name: answers.name,
560
+ domain: answers.domain || void 0,
561
+ industry: answers.industry || void 0,
562
+ website: answers.website || void 0
563
+ });
564
+ const data = JSON.parse(result);
565
+ console.log(`
566
+ Created account: ${data.account.id}
567
+ `);
568
+ await client.close();
569
+ });
570
+ cmd.command("delete <id>").description("Permanently delete an account (admin/owner only)").action(async (id) => {
571
+ const { default: inquirer } = await import("inquirer");
572
+ const { confirm } = await inquirer.prompt([
573
+ { type: "confirm", name: "confirm", message: `Delete account ${id}? This cannot be undone.`, default: false }
574
+ ]);
575
+ if (!confirm) {
576
+ console.log(" Cancelled.");
577
+ return;
578
+ }
579
+ const client = await getClient();
580
+ const result = await client.call("account_delete", { id });
581
+ const data = JSON.parse(result);
582
+ if (data.deleted) console.log(` Deleted.`);
583
+ await client.close();
584
+ });
585
+ return cmd;
586
+ }
587
+
588
+ // src/commands/opps.ts
589
+ import { Command as Command6 } from "commander";
590
+ function oppsCommand() {
591
+ const cmd = new Command6("opps").description("Manage opportunities");
592
+ cmd.command("list").option("--stage <stage>", "Filter by stage").option("-q, --query <query>", "Search query").action(async (opts) => {
593
+ const client = await getClient();
594
+ const result = await client.call("opportunity_search", { query: opts.query, stage: opts.stage, limit: 20 });
595
+ const data = JSON.parse(result);
596
+ if (data.opportunities?.length === 0) {
597
+ console.log("No opportunities found.");
598
+ return;
599
+ }
600
+ console.table(data.opportunities?.map((o) => ({
601
+ id: o.id.slice(0, 8),
602
+ name: o.name,
603
+ stage: o.stage,
604
+ amount: o.amount ?? 0,
605
+ close_date: o.close_date ?? ""
606
+ })));
607
+ if (data.total > 20) console.log(`
608
+ Showing 20 of ${data.total} opportunities`);
609
+ await client.close();
610
+ });
611
+ cmd.command("get <id>").description("Get opportunity details").action(async (id) => {
612
+ const client = await getClient();
613
+ const result = await client.call("opportunity_get", { id });
614
+ console.log(JSON.parse(result));
615
+ await client.close();
616
+ });
617
+ cmd.command("create").description("Create a new opportunity").action(async () => {
618
+ const { default: inquirer } = await import("inquirer");
619
+ const answers = await inquirer.prompt([
620
+ { type: "input", name: "name", message: "Opportunity name:" },
621
+ { type: "input", name: "account_id", message: "Account ID (optional):" },
622
+ { type: "input", name: "amount", message: "Amount (optional):" },
623
+ {
624
+ type: "list",
625
+ name: "stage",
626
+ message: "Stage:",
627
+ choices: ["prospecting", "qualification", "proposal", "negotiation", "closed_won", "closed_lost"],
628
+ default: "prospecting"
629
+ },
630
+ { type: "input", name: "close_date", message: "Close date YYYY-MM-DD (optional):" }
631
+ ]);
632
+ const client = await getClient();
633
+ const result = await client.call("opportunity_create", {
634
+ name: answers.name,
635
+ account_id: answers.account_id || void 0,
636
+ amount: answers.amount ? parseFloat(answers.amount) : void 0,
637
+ stage: answers.stage,
638
+ close_date: answers.close_date || void 0
639
+ });
640
+ const data = JSON.parse(result);
641
+ console.log(`
642
+ Created opportunity: ${data.opportunity.id}
643
+ `);
644
+ await client.close();
645
+ });
646
+ cmd.command("advance <id> <stage>").description("Advance opportunity to a new stage").option("--note <note>", "Optional note").option("--lost-reason <reason>", "Required when stage is closed_lost").action(async (id, stage, opts) => {
647
+ const client = await getClient();
648
+ const result = await client.call("opportunity_advance_stage", {
649
+ id,
650
+ stage,
651
+ note: opts.note,
652
+ lost_reason: opts.lostReason
653
+ });
654
+ const data = JSON.parse(result);
655
+ console.log(` Stage updated to: ${data.opportunity.stage}`);
656
+ await client.close();
657
+ });
658
+ cmd.command("delete <id>").description("Permanently delete an opportunity (admin/owner only)").action(async (id) => {
659
+ const { default: inquirer } = await import("inquirer");
660
+ const { confirm } = await inquirer.prompt([
661
+ { type: "confirm", name: "confirm", message: `Delete opportunity ${id}? This cannot be undone.`, default: false }
662
+ ]);
663
+ if (!confirm) {
664
+ console.log(" Cancelled.");
665
+ return;
666
+ }
667
+ const client = await getClient();
668
+ const result = await client.call("opportunity_delete", { id });
669
+ const data = JSON.parse(result);
670
+ if (data.deleted) console.log(` Deleted.`);
671
+ await client.close();
672
+ });
673
+ return cmd;
674
+ }
675
+
676
+ // src/commands/pipeline.ts
677
+ import { Command as Command7 } from "commander";
678
+ function pipelineCommand() {
679
+ return new Command7("pipeline").description("Show pipeline summary by stage").action(async () => {
680
+ const client = await getClient();
681
+ const result = await client.call("pipeline_summary", { group_by: "stage" });
682
+ const data = JSON.parse(result);
683
+ console.log(`
684
+ Pipeline: $${(data.total_value / 100).toLocaleString()} across ${data.count} opportunities
685
+ `);
686
+ if (data.by_stage?.length > 0) {
687
+ console.table(data.by_stage.map((s) => ({
688
+ stage: s.stage,
689
+ count: s.count,
690
+ value: `$${(s.value / 100).toLocaleString()}`
691
+ })));
692
+ }
693
+ await client.close();
694
+ });
695
+ }
696
+
697
+ // src/commands/search.ts
698
+ import { Command as Command8 } from "commander";
699
+ function searchCommand() {
700
+ return new Command8("search").description("Cross-entity search").argument("<query>", "Search query").action(async (query) => {
701
+ const client = await getClient();
702
+ const result = await client.call("crm_search", { query, limit: 10 });
703
+ const data = JSON.parse(result);
704
+ if (data.contacts?.length > 0) {
705
+ console.log("\n Contacts:");
706
+ console.table(data.contacts.map((c) => ({
707
+ id: c.id.slice(0, 8),
708
+ name: `${c.first_name} ${c.last_name}`,
709
+ email: c.email ?? ""
710
+ })));
711
+ }
712
+ if (data.accounts?.length > 0) {
713
+ console.log("\n Accounts:");
714
+ console.table(data.accounts.map((a) => ({
715
+ id: a.id.slice(0, 8),
716
+ name: a.name
717
+ })));
718
+ }
719
+ if (data.opportunities?.length > 0) {
720
+ console.log("\n Opportunities:");
721
+ console.table(data.opportunities.map((o) => ({
722
+ id: o.id.slice(0, 8),
723
+ name: o.name,
724
+ stage: o.stage
725
+ })));
726
+ }
727
+ const total = (data.contacts?.length ?? 0) + (data.accounts?.length ?? 0) + (data.opportunities?.length ?? 0);
728
+ if (total === 0) console.log(" No results found.");
729
+ await client.close();
730
+ });
731
+ }
732
+
733
+ // src/commands/hitl.ts
734
+ import { Command as Command9 } from "commander";
735
+ function hitlCommand() {
736
+ const cmd = new Command9("hitl").description("Manage HITL approval requests");
737
+ cmd.command("list").action(async () => {
738
+ const client = await getClient();
739
+ const result = await client.call("hitl_list_pending", { limit: 20 });
740
+ const data = JSON.parse(result);
741
+ if (data.requests?.length === 0) {
742
+ console.log("No pending HITL requests.");
743
+ return;
744
+ }
745
+ console.table(data.requests.map((r) => ({
746
+ id: r.id.slice(0, 8),
747
+ type: r.action_type,
748
+ summary: r.action_summary,
749
+ created: r.created_at
750
+ })));
751
+ await client.close();
752
+ });
753
+ cmd.command("approve <id>").action(async (id) => {
754
+ const client = await getClient();
755
+ const result = await client.call("hitl_resolve", { request_id: id, decision: "approved" });
756
+ console.log("Approved:", JSON.parse(result).request.id);
757
+ await client.close();
758
+ });
759
+ cmd.command("reject <id>").option("--note <note>", "Rejection note").action(async (id, opts) => {
760
+ const client = await getClient();
761
+ const result = await client.call("hitl_resolve", {
762
+ request_id: id,
763
+ decision: "rejected",
764
+ note: opts.note
765
+ });
766
+ console.log("Rejected:", JSON.parse(result).request.id);
767
+ await client.close();
768
+ });
769
+ return cmd;
770
+ }
771
+
772
+ // src/commands/events.ts
773
+ import { Command as Command10 } from "commander";
774
+ function eventsCommand() {
775
+ return new Command10("events").description("View audit log").option("--object <id>", "Filter by object ID").option("--type <type>", "Filter by event type").action(async (opts) => {
776
+ const config = loadConfigFile();
777
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
778
+ if (!databaseUrl) {
779
+ console.error("No database URL configured.");
780
+ process.exit(1);
781
+ }
782
+ process.env.CRMY_IMPORTED = "1";
783
+ const { initPool, closePool } = await import("@crmy/server");
784
+ const { searchEvents } = await import("@crmy/server/dist/db/repos/events.js");
785
+ const db = await initPool(databaseUrl);
786
+ const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
787
+ if (tenantResult.rows.length === 0) {
788
+ console.log("No tenant found. Run crmy init first.");
789
+ await closePool();
790
+ return;
791
+ }
792
+ const result = await searchEvents(db, tenantResult.rows[0].id, {
793
+ object_id: opts.object,
794
+ event_type: opts.type,
795
+ limit: 50
796
+ });
797
+ if (result.data.length === 0) {
798
+ console.log("No events found.");
799
+ } else {
800
+ console.table(result.data.map((e) => ({
801
+ id: e.id,
802
+ type: e.event_type,
803
+ actor: e.actor_type,
804
+ object: e.object_type,
805
+ created: e.created_at
806
+ })));
807
+ }
808
+ await closePool();
809
+ });
810
+ }
811
+
812
+ // src/commands/config.ts
813
+ import { Command as Command11 } from "commander";
814
+ function configCommand() {
815
+ return new Command11("config").description("Show configuration").command("show").action(() => {
816
+ const config = loadConfigFile();
817
+ if (Object.keys(config).length === 0) {
818
+ console.log("No .crmy.json found. Run `crmy init` to create one.");
819
+ return;
820
+ }
821
+ const display = {
822
+ ...config,
823
+ apiKey: config.apiKey ? config.apiKey.slice(0, 10) + "..." : void 0,
824
+ jwtSecret: config.jwtSecret ? "***" : void 0
825
+ };
826
+ console.log(JSON.stringify(display, null, 2));
827
+ });
828
+ }
829
+
830
+ // src/commands/migrate.ts
831
+ import { Command as Command12 } from "commander";
832
+ function migrateCommand() {
833
+ const cmd = new Command12("migrate").description("Database migrations");
834
+ cmd.command("run").action(async () => {
835
+ const config = loadConfigFile();
836
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
837
+ if (!databaseUrl) {
838
+ console.error("No database URL configured.");
839
+ process.exit(1);
840
+ }
841
+ process.env.CRMY_IMPORTED = "1";
842
+ const { initPool, closePool, runMigrations } = await import("@crmy/server");
843
+ const db = await initPool(databaseUrl);
844
+ const ran = await runMigrations(db);
845
+ if (ran.length === 0) {
846
+ console.log("No pending migrations.");
847
+ } else {
848
+ console.log(`Ran ${ran.length} migration(s): ${ran.join(", ")}`);
849
+ }
850
+ await closePool();
851
+ });
852
+ cmd.command("status").action(async () => {
853
+ const config = loadConfigFile();
854
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
855
+ if (!databaseUrl) {
856
+ console.error("No database URL configured.");
857
+ process.exit(1);
858
+ }
859
+ process.env.CRMY_IMPORTED = "1";
860
+ const { initPool, closePool } = await import("@crmy/server");
861
+ const { getMigrationStatus } = await import("@crmy/server/dist/db/migrate.js");
862
+ const db = await initPool(databaseUrl);
863
+ const status = await getMigrationStatus(db);
864
+ console.log("Applied:", status.applied.length ? status.applied.join(", ") : "(none)");
865
+ console.log("Pending:", status.pending.length ? status.pending.join(", ") : "(none)");
866
+ await closePool();
867
+ });
868
+ return cmd;
869
+ }
870
+
871
+ // src/commands/use-cases.ts
872
+ import { Command as Command13 } from "commander";
873
+ function useCasesCommand() {
874
+ const cmd = new Command13("use-cases").description("Manage use cases");
875
+ cmd.command("list").option("--account <id>", "Filter by account ID").option("--stage <stage>", "Filter by stage").option("-q, --query <query>", "Search query").action(async (opts) => {
876
+ const client = await getClient();
877
+ const result = await client.call("use_case_search", {
878
+ account_id: opts.account,
879
+ stage: opts.stage,
880
+ query: opts.query,
881
+ limit: 20
882
+ });
883
+ const data = JSON.parse(result);
884
+ if (data.use_cases?.length === 0) {
885
+ console.log("No use cases found.");
886
+ return;
887
+ }
888
+ console.table(data.use_cases?.map((uc) => ({
889
+ id: uc.id.slice(0, 8),
890
+ name: uc.name,
891
+ stage: uc.stage,
892
+ arr: uc.attributed_arr ?? "",
893
+ health: uc.health_score ?? ""
894
+ })));
895
+ if (data.total > 20) console.log(`
896
+ Showing 20 of ${data.total} use cases`);
897
+ await client.close();
898
+ });
899
+ cmd.command("get <id>").action(async (id) => {
900
+ const client = await getClient();
901
+ const result = await client.call("use_case_get", { id });
902
+ console.log(JSON.parse(result));
903
+ await client.close();
904
+ });
905
+ cmd.command("create").action(async () => {
906
+ const { default: inquirer } = await import("inquirer");
907
+ const answers = await inquirer.prompt([
908
+ { type: "input", name: "account_id", message: "Account ID:" },
909
+ { type: "input", name: "name", message: "Use case name:" },
910
+ { type: "input", name: "description", message: "Description:" }
911
+ ]);
912
+ const client = await getClient();
913
+ const result = await client.call("use_case_create", {
914
+ account_id: answers.account_id,
915
+ name: answers.name,
916
+ description: answers.description || void 0
917
+ });
918
+ const data = JSON.parse(result);
919
+ console.log(`
920
+ Created use case: ${data.use_case.id}
921
+ `);
922
+ await client.close();
923
+ });
924
+ cmd.command("summary").option("--account <id>", "Filter by account ID").option("--group-by <field>", "Group by: stage, product_line, owner", "stage").action(async (opts) => {
925
+ const client = await getClient();
926
+ const result = await client.call("use_case_summary", {
927
+ account_id: opts.account,
928
+ group_by: opts.groupBy
929
+ });
930
+ const data = JSON.parse(result);
931
+ console.table(data.summary);
932
+ await client.close();
933
+ });
934
+ cmd.command("delete <id>").description("Delete a use case (admin/owner only)").action(async (id) => {
935
+ const { default: inquirer } = await import("inquirer");
936
+ const { confirm } = await inquirer.prompt([
937
+ { type: "confirm", name: "confirm", message: `Delete use case ${id}? This cannot be undone.`, default: false }
938
+ ]);
939
+ if (!confirm) {
940
+ console.log(" Cancelled.");
941
+ return;
942
+ }
943
+ const client = await getClient();
944
+ const result = await client.call("use_case_delete", { id });
945
+ const data = JSON.parse(result);
946
+ if (data.deleted) console.log(` Deleted.`);
947
+ await client.close();
948
+ });
949
+ return cmd;
950
+ }
951
+
952
+ // src/commands/webhooks.ts
953
+ import { Command as Command14 } from "commander";
954
+ function webhooksCommand() {
955
+ const cmd = new Command14("webhooks").description("Manage webhook endpoints");
956
+ cmd.command("list").option("--active", "Show only active webhooks").action(async (opts) => {
957
+ const client = await getClient();
958
+ const result = await client.call("webhook_list", {
959
+ active: opts.active ?? void 0,
960
+ limit: 20
961
+ });
962
+ const data = JSON.parse(result);
963
+ if (data.webhooks?.length === 0) {
964
+ console.log("No webhooks found.");
965
+ return;
966
+ }
967
+ console.table(data.webhooks?.map((w) => ({
968
+ id: w.id.slice(0, 8),
969
+ url: w.url,
970
+ events: w.event_types?.join(", "),
971
+ active: w.is_active
972
+ })));
973
+ await client.close();
974
+ });
975
+ cmd.command("get <id>").action(async (id) => {
976
+ const client = await getClient();
977
+ const result = await client.call("webhook_get", { id });
978
+ console.log(JSON.parse(result));
979
+ await client.close();
980
+ });
981
+ cmd.command("create").action(async () => {
982
+ const { default: inquirer } = await import("inquirer");
983
+ const answers = await inquirer.prompt([
984
+ { type: "input", name: "url", message: "Webhook URL:" },
985
+ { type: "input", name: "events", message: "Events (comma-separated):" },
986
+ { type: "input", name: "description", message: "Description:" }
987
+ ]);
988
+ const client = await getClient();
989
+ const result = await client.call("webhook_create", {
990
+ url: answers.url,
991
+ events: answers.events.split(",").map((e) => e.trim()),
992
+ description: answers.description || void 0
993
+ });
994
+ const data = JSON.parse(result);
995
+ console.log(`
996
+ Created webhook: ${data.webhook.id}
997
+ `);
998
+ console.log(` Secret: ${data.webhook.secret}
999
+ `);
1000
+ await client.close();
1001
+ });
1002
+ cmd.command("delete <id>").action(async (id) => {
1003
+ const client = await getClient();
1004
+ const result = await client.call("webhook_delete", { id });
1005
+ console.log(JSON.parse(result));
1006
+ await client.close();
1007
+ });
1008
+ cmd.command("deliveries").option("--endpoint <id>", "Filter by endpoint ID").option("--status <status>", "Filter by status (pending, success, failed)").action(async (opts) => {
1009
+ const client = await getClient();
1010
+ const result = await client.call("webhook_list_deliveries", {
1011
+ endpoint_id: opts.endpoint,
1012
+ status: opts.status,
1013
+ limit: 20
1014
+ });
1015
+ const data = JSON.parse(result);
1016
+ if (data.deliveries?.length === 0) {
1017
+ console.log("No deliveries found.");
1018
+ return;
1019
+ }
1020
+ console.table(data.deliveries?.map((d) => ({
1021
+ id: d.id.slice(0, 8),
1022
+ event_type: d.event_type,
1023
+ status: d.status,
1024
+ attempts: d.attempt_count,
1025
+ created: d.created_at
1026
+ })));
1027
+ await client.close();
1028
+ });
1029
+ return cmd;
1030
+ }
1031
+
1032
+ // src/commands/emails.ts
1033
+ import { Command as Command15 } from "commander";
1034
+ function emailsCommand() {
1035
+ const cmd = new Command15("emails").description("Manage outbound emails");
1036
+ cmd.command("list").option("--contact <id>", "Filter by contact ID").option("--status <status>", "Filter by status").action(async (opts) => {
1037
+ const client = await getClient();
1038
+ const result = await client.call("email_search", {
1039
+ contact_id: opts.contact,
1040
+ status: opts.status,
1041
+ limit: 20
1042
+ });
1043
+ const data = JSON.parse(result);
1044
+ if (data.emails?.length === 0) {
1045
+ console.log("No emails found.");
1046
+ return;
1047
+ }
1048
+ console.table(data.emails?.map((e) => ({
1049
+ id: e.id.slice(0, 8),
1050
+ to: e.to_email,
1051
+ subject: e.subject,
1052
+ status: e.status,
1053
+ created: e.created_at
1054
+ })));
1055
+ await client.close();
1056
+ });
1057
+ cmd.command("get <id>").action(async (id) => {
1058
+ const client = await getClient();
1059
+ const result = await client.call("email_get", { id });
1060
+ console.log(JSON.parse(result));
1061
+ await client.close();
1062
+ });
1063
+ cmd.command("create").action(async () => {
1064
+ const { default: inquirer } = await import("inquirer");
1065
+ const answers = await inquirer.prompt([
1066
+ { type: "input", name: "to_address", message: "To (email):" },
1067
+ { type: "input", name: "subject", message: "Subject:" },
1068
+ { type: "input", name: "body_text", message: "Body:" },
1069
+ { type: "input", name: "contact_id", message: "Contact ID (optional):" },
1070
+ { type: "confirm", name: "require_approval", message: "Require HITL approval?", default: true }
1071
+ ]);
1072
+ const client = await getClient();
1073
+ const result = await client.call("email_create", {
1074
+ to_address: answers.to_address,
1075
+ subject: answers.subject,
1076
+ body_text: answers.body_text,
1077
+ contact_id: answers.contact_id || void 0,
1078
+ require_approval: answers.require_approval
1079
+ });
1080
+ const data = JSON.parse(result);
1081
+ console.log(`
1082
+ Created email: ${data.email.id} status: ${data.email.status}
1083
+ `);
1084
+ if (data.hitl_request_id) {
1085
+ console.log(` HITL approval required: ${data.hitl_request_id}
1086
+ `);
1087
+ }
1088
+ await client.close();
1089
+ });
1090
+ return cmd;
1091
+ }
1092
+
1093
+ // src/commands/custom-fields.ts
1094
+ import { Command as Command16 } from "commander";
1095
+ function customFieldsCommand() {
1096
+ const cmd = new Command16("custom-fields").description("Manage custom field definitions");
1097
+ cmd.command("list <object_type>").description("List custom fields for an object type (contact, account, opportunity, activity, use_case)").action(async (objectType) => {
1098
+ const client = await getClient();
1099
+ const result = await client.call("custom_field_list", { object_type: objectType });
1100
+ const data = JSON.parse(result);
1101
+ if (data.fields?.length === 0) {
1102
+ console.log(`No custom fields defined for ${objectType}.`);
1103
+ return;
1104
+ }
1105
+ console.table(data.fields?.map((f) => ({
1106
+ id: f.id.slice(0, 8),
1107
+ key: f.field_key,
1108
+ label: f.label,
1109
+ type: f.field_type,
1110
+ required: f.is_required
1111
+ })));
1112
+ await client.close();
1113
+ });
1114
+ cmd.command("create").action(async () => {
1115
+ const { default: inquirer } = await import("inquirer");
1116
+ const answers = await inquirer.prompt([
1117
+ {
1118
+ type: "list",
1119
+ name: "object_type",
1120
+ message: "Object type:",
1121
+ choices: ["contact", "account", "opportunity", "activity", "use_case"]
1122
+ },
1123
+ { type: "input", name: "field_name", message: "Field key (snake_case):" },
1124
+ { type: "input", name: "label", message: "Display label:" },
1125
+ {
1126
+ type: "list",
1127
+ name: "field_type",
1128
+ message: "Field type:",
1129
+ choices: ["text", "number", "boolean", "date", "select", "multi_select"]
1130
+ },
1131
+ { type: "confirm", name: "required", message: "Required?", default: false }
1132
+ ]);
1133
+ const client = await getClient();
1134
+ const result = await client.call("custom_field_create", answers);
1135
+ const data = JSON.parse(result);
1136
+ console.log(`
1137
+ Created custom field: ${data.field.id}
1138
+ `);
1139
+ await client.close();
1140
+ });
1141
+ cmd.command("delete <id>").action(async (id) => {
1142
+ const client = await getClient();
1143
+ const result = await client.call("custom_field_delete", { id });
1144
+ console.log(JSON.parse(result));
1145
+ await client.close();
1146
+ });
1147
+ return cmd;
1148
+ }
1149
+
1150
+ // src/commands/notes.ts
1151
+ import { Command as Command17 } from "commander";
1152
+ function notesCommand() {
1153
+ const cmd = new Command17("notes").description("Manage notes and comments on CRM objects");
1154
+ cmd.command("list <object_type> <object_id>").description("List notes for an object (contact, account, opportunity, use_case)").option("--visibility <vis>", "Filter: internal or external").option("--pinned", "Show only pinned notes").action(async (objectType, objectId, opts) => {
1155
+ const client = await getClient();
1156
+ const result = await client.call("note_list", {
1157
+ object_type: objectType,
1158
+ object_id: objectId,
1159
+ visibility: opts.visibility,
1160
+ pinned: opts.pinned ?? void 0,
1161
+ limit: 20
1162
+ });
1163
+ const data = JSON.parse(result);
1164
+ if (data.notes?.length === 0) {
1165
+ console.log("No notes found.");
1166
+ return;
1167
+ }
1168
+ for (const n of data.notes ?? []) {
1169
+ const pin = n.pinned ? " \u{1F4CC}" : "";
1170
+ const vis = n.visibility === "external" ? " [external]" : "";
1171
+ console.log(` [${n.id.slice(0, 8)}]${pin}${vis} ${n.author_type}/${n.author_id ?? "anon"}`);
1172
+ console.log(` ${n.body.slice(0, 120)}`);
1173
+ console.log(` ${n.created_at}
1174
+ `);
1175
+ }
1176
+ if (data.total > 20) console.log(` Showing 20 of ${data.total} notes`);
1177
+ await client.close();
1178
+ });
1179
+ cmd.command("add <object_type> <object_id>").description("Add a note to an object").option("--parent <id>", "Reply to a note (thread)").option("--external", "Make note visible externally").option("--pin", "Pin this note").action(async (objectType, objectId, opts) => {
1180
+ const { default: inquirer } = await import("inquirer");
1181
+ const answers = await inquirer.prompt([
1182
+ { type: "input", name: "body", message: "Note:" }
1183
+ ]);
1184
+ const client = await getClient();
1185
+ const result = await client.call("note_create", {
1186
+ object_type: objectType,
1187
+ object_id: objectId,
1188
+ body: answers.body,
1189
+ parent_id: opts.parent,
1190
+ visibility: opts.external ? "external" : "internal",
1191
+ pinned: opts.pin ?? false
1192
+ });
1193
+ const data = JSON.parse(result);
1194
+ console.log(`
1195
+ Note created: ${data.note.id}
1196
+ `);
1197
+ await client.close();
1198
+ });
1199
+ cmd.command("get <id>").action(async (id) => {
1200
+ const client = await getClient();
1201
+ const result = await client.call("note_get", { id });
1202
+ const data = JSON.parse(result);
1203
+ console.log(`
1204
+ ${data.note.body}
1205
+ `);
1206
+ if (data.replies?.length > 0) {
1207
+ console.log(` ${data.replies.length} replies:`);
1208
+ for (const r of data.replies) {
1209
+ console.log(` [${r.id.slice(0, 8)}] ${r.body.slice(0, 80)}`);
1210
+ }
1211
+ }
1212
+ await client.close();
1213
+ });
1214
+ cmd.command("delete <id>").action(async (id) => {
1215
+ const client = await getClient();
1216
+ const result = await client.call("note_delete", { id });
1217
+ console.log(JSON.parse(result));
1218
+ await client.close();
1219
+ });
1220
+ return cmd;
1221
+ }
1222
+
1223
+ // src/commands/workflows.ts
1224
+ import { Command as Command18 } from "commander";
1225
+ function workflowsCommand() {
1226
+ const cmd = new Command18("workflows").description("Manage automation workflows");
1227
+ cmd.command("list").option("--trigger <event>", "Filter by trigger event").option("--active", "Show only active workflows").action(async (opts) => {
1228
+ const client = await getClient();
1229
+ const result = await client.call("workflow_list", {
1230
+ trigger_event: opts.trigger,
1231
+ is_active: opts.active ?? void 0,
1232
+ limit: 20
1233
+ });
1234
+ const data = JSON.parse(result);
1235
+ if (data.workflows?.length === 0) {
1236
+ console.log("No workflows found.");
1237
+ return;
1238
+ }
1239
+ console.table(data.workflows?.map((w) => ({
1240
+ id: w.id.slice(0, 8),
1241
+ name: w.name,
1242
+ trigger: w.trigger_event,
1243
+ active: w.is_active,
1244
+ runs: w.run_count
1245
+ })));
1246
+ await client.close();
1247
+ });
1248
+ cmd.command("get <id>").action(async (id) => {
1249
+ const client = await getClient();
1250
+ const result = await client.call("workflow_get", { id });
1251
+ const data = JSON.parse(result);
1252
+ console.log("\nWorkflow:", data.workflow.name);
1253
+ console.log("Trigger:", data.workflow.trigger_event);
1254
+ console.log("Active:", data.workflow.is_active);
1255
+ console.log("Actions:", JSON.stringify(data.workflow.actions, null, 2));
1256
+ if (data.recent_runs?.length > 0) {
1257
+ console.log("\nRecent runs:");
1258
+ console.table(data.recent_runs.map((r) => ({
1259
+ id: r.id.slice(0, 8),
1260
+ status: r.status,
1261
+ actions: `${r.actions_run}/${r.actions_total}`,
1262
+ started: r.started_at
1263
+ })));
1264
+ }
1265
+ await client.close();
1266
+ });
1267
+ cmd.command("create").action(async () => {
1268
+ const { default: inquirer } = await import("inquirer");
1269
+ const answers = await inquirer.prompt([
1270
+ { type: "input", name: "name", message: "Workflow name:" },
1271
+ { type: "input", name: "trigger_event", message: "Trigger event (e.g. contact.created):" },
1272
+ { type: "input", name: "description", message: "Description:" }
1273
+ ]);
1274
+ const actionAnswers = await inquirer.prompt([
1275
+ {
1276
+ type: "list",
1277
+ name: "type",
1278
+ message: "Action type:",
1279
+ choices: ["send_notification", "create_activity", "create_note", "add_tag", "webhook"]
1280
+ },
1281
+ { type: "input", name: "message", message: "Action message/body:" }
1282
+ ]);
1283
+ const client = await getClient();
1284
+ const result = await client.call("workflow_create", {
1285
+ name: answers.name,
1286
+ description: answers.description || void 0,
1287
+ trigger_event: answers.trigger_event,
1288
+ actions: [{ type: actionAnswers.type, config: { message: actionAnswers.message } }]
1289
+ });
1290
+ const data = JSON.parse(result);
1291
+ console.log(`
1292
+ Created workflow: ${data.workflow.id}
1293
+ `);
1294
+ await client.close();
1295
+ });
1296
+ cmd.command("delete <id>").action(async (id) => {
1297
+ const client = await getClient();
1298
+ const result = await client.call("workflow_delete", { id });
1299
+ console.log(JSON.parse(result));
1300
+ await client.close();
1301
+ });
1302
+ cmd.command("runs <workflow_id>").option("--status <status>", "Filter: running, completed, failed").action(async (workflowId, opts) => {
1303
+ const client = await getClient();
1304
+ const result = await client.call("workflow_run_list", {
1305
+ workflow_id: workflowId,
1306
+ status: opts.status,
1307
+ limit: 20
1308
+ });
1309
+ const data = JSON.parse(result);
1310
+ if (data.runs?.length === 0) {
1311
+ console.log("No runs found.");
1312
+ return;
1313
+ }
1314
+ console.table(data.runs?.map((r) => ({
1315
+ id: r.id.slice(0, 8),
1316
+ status: r.status,
1317
+ actions: `${r.actions_run}/${r.actions_total}`,
1318
+ error: r.error ?? "",
1319
+ started: r.started_at
1320
+ })));
1321
+ await client.close();
1322
+ });
1323
+ return cmd;
1324
+ }
1325
+
1326
+ // src/commands/actors.ts
1327
+ import { Command as Command19 } from "commander";
1328
+ function actorsCommand() {
1329
+ const cmd = new Command19("actors").description("Manage actors (humans & agents)");
1330
+ cmd.command("list").option("--type <type>", "Filter by actor_type (human or agent)").option("-q, --query <query>", "Search query").action(async (opts) => {
1331
+ const client = await getClient();
1332
+ const result = await client.call("actor_list", {
1333
+ actor_type: opts.type,
1334
+ query: opts.query,
1335
+ limit: 20
1336
+ });
1337
+ const data = JSON.parse(result);
1338
+ if (data.actors?.length === 0) {
1339
+ console.log("No actors found.");
1340
+ return;
1341
+ }
1342
+ console.table(data.actors?.map((a) => ({
1343
+ id: a.id.slice(0, 8),
1344
+ type: a.actor_type,
1345
+ name: a.display_name,
1346
+ email: a.email ?? "",
1347
+ agent_id: a.agent_identifier ?? "",
1348
+ active: a.is_active ? "\u2713" : "\u2717"
1349
+ })));
1350
+ if (data.total > 20) console.log(`
1351
+ Showing 20 of ${data.total} actors`);
1352
+ await client.close();
1353
+ });
1354
+ cmd.command("register").description("Register a new actor").action(async () => {
1355
+ const { default: inquirer } = await import("inquirer");
1356
+ const answers = await inquirer.prompt([
1357
+ { type: "list", name: "actor_type", message: "Actor type:", choices: ["human", "agent"] },
1358
+ { type: "input", name: "display_name", message: "Display name:" },
1359
+ { type: "input", name: "email", message: "Email (for humans):" },
1360
+ { type: "input", name: "agent_identifier", message: "Agent identifier (for agents):" },
1361
+ { type: "input", name: "agent_model", message: "Agent model (e.g. claude-sonnet-4-20250514):" }
1362
+ ]);
1363
+ const client = await getClient();
1364
+ const result = await client.call("actor_register", {
1365
+ actor_type: answers.actor_type,
1366
+ display_name: answers.display_name,
1367
+ email: answers.email || void 0,
1368
+ agent_identifier: answers.agent_identifier || void 0,
1369
+ agent_model: answers.agent_model || void 0
1370
+ });
1371
+ const data = JSON.parse(result);
1372
+ console.log(`
1373
+ Registered actor: ${data.actor.id}
1374
+ `);
1375
+ await client.close();
1376
+ });
1377
+ cmd.command("get <id>").action(async (id) => {
1378
+ const client = await getClient();
1379
+ const result = await client.call("actor_get", { id });
1380
+ console.log(JSON.parse(result));
1381
+ await client.close();
1382
+ });
1383
+ cmd.command("whoami").description("Show current actor identity").action(async () => {
1384
+ const client = await getClient();
1385
+ const result = await client.call("actor_whoami", {});
1386
+ console.log(JSON.parse(result));
1387
+ await client.close();
1388
+ });
1389
+ return cmd;
1390
+ }
1391
+
1392
+ // src/commands/assignments.ts
1393
+ import { Command as Command20 } from "commander";
1394
+ function assignmentsCommand() {
1395
+ const cmd = new Command20("assignments").description("Manage assignments (coordination & handoffs)");
1396
+ cmd.command("list").option("--mine", "Show assignments assigned to me").option("--delegated", "Show assignments I created").option("--status <status>", "Filter by status").option("--priority <priority>", "Filter by priority").action(async (opts) => {
1397
+ const client = await getClient();
1398
+ const input = { limit: 20 };
1399
+ if (opts.mine) {
1400
+ const whoami = JSON.parse(await client.call("actor_whoami", {}));
1401
+ input.assigned_to = whoami.actor_id;
1402
+ }
1403
+ if (opts.delegated) {
1404
+ const whoami = JSON.parse(await client.call("actor_whoami", {}));
1405
+ input.assigned_by = whoami.actor_id;
1406
+ }
1407
+ if (opts.status) input.status = opts.status;
1408
+ if (opts.priority) input.priority = opts.priority;
1409
+ const result = await client.call("assignment_list", input);
1410
+ const data = JSON.parse(result);
1411
+ if (data.assignments?.length === 0) {
1412
+ console.log("No assignments found.");
1413
+ return;
1414
+ }
1415
+ console.table(data.assignments?.map((a) => ({
1416
+ id: a.id.slice(0, 8),
1417
+ title: a.title.slice(0, 40),
1418
+ type: a.assignment_type,
1419
+ status: a.status,
1420
+ priority: a.priority,
1421
+ subject: `${a.subject_type}:${a.subject_id.slice(0, 8)}`
1422
+ })));
1423
+ if (data.total > 20) console.log(`
1424
+ Showing 20 of ${data.total} assignments`);
1425
+ await client.close();
1426
+ });
1427
+ cmd.command("create").description("Create a new assignment").action(async () => {
1428
+ const { default: inquirer } = await import("inquirer");
1429
+ const answers = await inquirer.prompt([
1430
+ { type: "input", name: "title", message: "Title:" },
1431
+ { type: "input", name: "description", message: "Description:" },
1432
+ { type: "list", name: "assignment_type", message: "Type:", choices: ["follow_up", "review", "approve", "send", "call", "meet", "research", "draft", "custom"] },
1433
+ { type: "input", name: "assigned_to", message: "Assign to (actor UUID):" },
1434
+ { type: "list", name: "subject_type", message: "Subject type:", choices: ["contact", "account", "opportunity", "use_case"] },
1435
+ { type: "input", name: "subject_id", message: "Subject ID (UUID):" },
1436
+ { type: "list", name: "priority", message: "Priority:", choices: ["low", "normal", "high", "urgent"], default: "normal" },
1437
+ { type: "input", name: "context", message: "Context / handoff notes:" }
1438
+ ]);
1439
+ const client = await getClient();
1440
+ const result = await client.call("assignment_create", {
1441
+ title: answers.title,
1442
+ description: answers.description || void 0,
1443
+ assignment_type: answers.assignment_type,
1444
+ assigned_to: answers.assigned_to,
1445
+ subject_type: answers.subject_type,
1446
+ subject_id: answers.subject_id,
1447
+ priority: answers.priority,
1448
+ context: answers.context || void 0
1449
+ });
1450
+ const data = JSON.parse(result);
1451
+ console.log(`
1452
+ Created assignment: ${data.assignment.id}
1453
+ `);
1454
+ await client.close();
1455
+ });
1456
+ cmd.command("get <id>").action(async (id) => {
1457
+ const client = await getClient();
1458
+ const result = await client.call("assignment_get", { id });
1459
+ console.log(JSON.parse(result));
1460
+ await client.close();
1461
+ });
1462
+ cmd.command("accept <id>").description("Accept a pending assignment").action(async (id) => {
1463
+ const client = await getClient();
1464
+ const result = await client.call("assignment_accept", { id });
1465
+ const data = JSON.parse(result);
1466
+ console.log(`
1467
+ Accepted assignment: ${data.assignment.id} (status: ${data.assignment.status})
1468
+ `);
1469
+ await client.close();
1470
+ });
1471
+ cmd.command("complete <id>").option("--activity <activityId>", "Link the completing activity").description("Complete an assignment").action(async (id, opts) => {
1472
+ const client = await getClient();
1473
+ const result = await client.call("assignment_complete", {
1474
+ id,
1475
+ completed_by_activity_id: opts.activity || void 0
1476
+ });
1477
+ const data = JSON.parse(result);
1478
+ console.log(`
1479
+ Completed assignment: ${data.assignment.id}
1480
+ `);
1481
+ await client.close();
1482
+ });
1483
+ cmd.command("decline <id>").option("-r, --reason <reason>", "Reason for declining").description("Decline an assignment").action(async (id, opts) => {
1484
+ const client = await getClient();
1485
+ const result = await client.call("assignment_decline", {
1486
+ id,
1487
+ reason: opts.reason || void 0
1488
+ });
1489
+ const data = JSON.parse(result);
1490
+ console.log(`
1491
+ Declined assignment: ${data.assignment.id}
1492
+ `);
1493
+ await client.close();
1494
+ });
1495
+ cmd.command("start <id>").description("Start working on an accepted assignment").action(async (id) => {
1496
+ const client = await getClient();
1497
+ const result = await client.call("assignment_start", { id });
1498
+ const data = JSON.parse(result);
1499
+ console.log(`
1500
+ Started assignment: ${data.assignment.id} (status: ${data.assignment.status})
1501
+ `);
1502
+ await client.close();
1503
+ });
1504
+ cmd.command("block <id>").option("-r, --reason <reason>", "Reason for blocking").description("Mark an assignment as blocked").action(async (id, opts) => {
1505
+ const client = await getClient();
1506
+ const result = await client.call("assignment_block", {
1507
+ id,
1508
+ reason: opts.reason || void 0
1509
+ });
1510
+ const data = JSON.parse(result);
1511
+ console.log(`
1512
+ Blocked assignment: ${data.assignment.id}
1513
+ `);
1514
+ await client.close();
1515
+ });
1516
+ cmd.command("cancel <id>").option("-r, --reason <reason>", "Reason for cancelling").description("Cancel an assignment").action(async (id, opts) => {
1517
+ const client = await getClient();
1518
+ const result = await client.call("assignment_cancel", {
1519
+ id,
1520
+ reason: opts.reason || void 0
1521
+ });
1522
+ const data = JSON.parse(result);
1523
+ console.log(`
1524
+ Cancelled assignment: ${data.assignment.id}
1525
+ `);
1526
+ await client.close();
1527
+ });
1528
+ return cmd;
1529
+ }
1530
+
1531
+ // src/commands/context.ts
1532
+ import { Command as Command21 } from "commander";
1533
+ function contextCommand() {
1534
+ const cmd = new Command21("context").description("Manage context entries (knowledge & memory)");
1535
+ cmd.command("list").option("--subject-type <type>", "Filter by subject type (contact, account, opportunity, use_case)").option("--subject-id <id>", "Filter by subject ID").option("--type <contextType>", "Filter by context type (note, research, objection, etc.)").option("--current-only", "Only show current entries (default behavior)").action(async (opts) => {
1536
+ const client = await getClient();
1537
+ const result = await client.call("context_list", {
1538
+ subject_type: opts.subjectType,
1539
+ subject_id: opts.subjectId,
1540
+ context_type: opts.type,
1541
+ is_current: opts.currentOnly ? true : void 0,
1542
+ limit: 20
1543
+ });
1544
+ const data = JSON.parse(result);
1545
+ if (data.context_entries?.length === 0) {
1546
+ console.log("No context entries found.");
1547
+ return;
1548
+ }
1549
+ console.table(data.context_entries?.map((c) => ({
1550
+ id: c.id.slice(0, 8),
1551
+ type: c.context_type,
1552
+ title: (c.title ?? "").slice(0, 40),
1553
+ subject: `${c.subject_type}:${c.subject_id.slice(0, 8)}`,
1554
+ confidence: c.confidence ?? "\u2014",
1555
+ current: c.is_current ? "\u2713" : "\u2717"
1556
+ })));
1557
+ if (data.total > 20) console.log(`
1558
+ Showing 20 of ${data.total} entries`);
1559
+ await client.close();
1560
+ });
1561
+ cmd.command("add").description("Add context about a CRM object").action(async () => {
1562
+ const { default: inquirer } = await import("inquirer");
1563
+ const answers = await inquirer.prompt([
1564
+ { type: "list", name: "subject_type", message: "Subject type:", choices: ["contact", "account", "opportunity", "use_case"] },
1565
+ { type: "input", name: "subject_id", message: "Subject ID (UUID):" },
1566
+ { type: "list", name: "context_type", message: "Context type:", choices: ["note", "transcript", "summary", "research", "preference", "objection", "competitive_intel", "relationship_map", "meeting_notes", "agent_reasoning"] },
1567
+ { type: "input", name: "title", message: "Title (optional):" },
1568
+ { type: "editor", name: "body", message: "Body:" },
1569
+ { type: "input", name: "confidence", message: "Confidence (0.0\u20131.0, optional):" },
1570
+ { type: "input", name: "source", message: "Source (e.g. manual, call_transcript, agent_research):" }
1571
+ ]);
1572
+ const client = await getClient();
1573
+ const result = await client.call("context_add", {
1574
+ subject_type: answers.subject_type,
1575
+ subject_id: answers.subject_id,
1576
+ context_type: answers.context_type,
1577
+ title: answers.title || void 0,
1578
+ body: answers.body,
1579
+ confidence: answers.confidence ? parseFloat(answers.confidence) : void 0,
1580
+ source: answers.source || void 0
1581
+ });
1582
+ const data = JSON.parse(result);
1583
+ console.log(`
1584
+ Added context: ${data.context_entry.id}
1585
+ `);
1586
+ await client.close();
1587
+ });
1588
+ cmd.command("get <id>").action(async (id) => {
1589
+ const client = await getClient();
1590
+ const result = await client.call("context_get", { id });
1591
+ console.log(JSON.parse(result));
1592
+ await client.close();
1593
+ });
1594
+ cmd.command("supersede <id>").option("-b, --body <body>", "New body text").option("-t, --title <title>", "New title").description("Supersede an existing context entry with updated content").action(async (id, opts) => {
1595
+ let body = opts.body;
1596
+ if (!body) {
1597
+ const { default: inquirer } = await import("inquirer");
1598
+ const answers = await inquirer.prompt([
1599
+ { type: "editor", name: "body", message: "Updated body:" }
1600
+ ]);
1601
+ body = answers.body;
1602
+ }
1603
+ const client = await getClient();
1604
+ const result = await client.call("context_supersede", {
1605
+ id,
1606
+ body,
1607
+ title: opts.title || void 0
1608
+ });
1609
+ const data = JSON.parse(result);
1610
+ console.log(`
1611
+ Superseded with new entry: ${data.context_entry.id}
1612
+ `);
1613
+ await client.close();
1614
+ });
1615
+ cmd.command("search <query>").description("Full-text search across context entries").option("--subject <subject>", "Filter by subject (type:UUID)").option("--type <contextType>", "Filter by context type").option("--tag <tag>", "Filter by tag").option("--include-superseded", "Include non-current entries").option("--limit <n>", "Max results", "20").action(async (query, opts) => {
1616
+ const input = {
1617
+ query,
1618
+ limit: parseInt(opts.limit, 10),
1619
+ current_only: !opts.includeSuperseded
1620
+ };
1621
+ if (opts.subject) {
1622
+ const [st, si] = opts.subject.split(":");
1623
+ input.subject_type = st;
1624
+ input.subject_id = si;
1625
+ }
1626
+ if (opts.type) input.context_type = opts.type;
1627
+ if (opts.tag) input.tag = opts.tag;
1628
+ const client = await getClient();
1629
+ const result = await client.call("context_search", input);
1630
+ const data = JSON.parse(result);
1631
+ if (data.context_entries?.length === 0) {
1632
+ console.log("No results found.");
1633
+ return;
1634
+ }
1635
+ console.table(data.context_entries?.map((c) => ({
1636
+ id: c.id.slice(0, 8),
1637
+ type: c.context_type,
1638
+ title: (c.title ?? "").slice(0, 40),
1639
+ subject: `${c.subject_type}:${c.subject_id.slice(0, 8)}`,
1640
+ confidence: c.confidence ?? "\u2014"
1641
+ })));
1642
+ await client.close();
1643
+ });
1644
+ cmd.command("review <id>").description("Mark a context entry as reviewed (still accurate)").action(async (id) => {
1645
+ const client = await getClient();
1646
+ const result = await client.call("context_review", { id });
1647
+ const data = JSON.parse(result);
1648
+ console.log(`
1649
+ Reviewed context entry: ${data.context_entry.id} (reviewed_at: ${data.context_entry.reviewed_at})
1650
+ `);
1651
+ await client.close();
1652
+ });
1653
+ cmd.command("stale").description("List stale context entries that need review").option("--subject <subject>", "Filter by subject (type:UUID)").option("--limit <n>", "Max results", "20").action(async (opts) => {
1654
+ const input = { limit: parseInt(opts.limit, 10) };
1655
+ if (opts.subject) {
1656
+ const [st, si] = opts.subject.split(":");
1657
+ input.subject_type = st;
1658
+ input.subject_id = si;
1659
+ }
1660
+ const client = await getClient();
1661
+ const result = await client.call("context_stale", input);
1662
+ const data = JSON.parse(result);
1663
+ if (data.stale_entries?.length === 0) {
1664
+ console.log("No stale entries found.");
1665
+ return;
1666
+ }
1667
+ console.table(data.stale_entries?.map((c) => ({
1668
+ id: c.id.slice(0, 8),
1669
+ type: c.context_type,
1670
+ title: (c.title ?? "").slice(0, 40),
1671
+ expired: c.valid_until,
1672
+ subject: `${c.subject_type}:${c.subject_id.slice(0, 8)}`
1673
+ })));
1674
+ await client.close();
1675
+ });
1676
+ return cmd;
1677
+ }
1678
+
1679
+ // src/commands/activity-types.ts
1680
+ import { Command as Command22 } from "commander";
1681
+ function activityTypesCommand() {
1682
+ const cmd = new Command22("activity-types").description("Manage activity type registry");
1683
+ cmd.command("list").option("--category <cat>", "Filter by category (outreach, meeting, proposal, contract, internal, lifecycle, handoff)").action(async (opts) => {
1684
+ const client = await getClient();
1685
+ const result = await client.call("activity_type_list", { category: opts.category });
1686
+ const data = JSON.parse(result);
1687
+ if (data.activity_types?.length === 0) {
1688
+ console.log("No activity types found.");
1689
+ return;
1690
+ }
1691
+ console.table(data.activity_types?.map((t) => ({
1692
+ type_name: t.type_name,
1693
+ label: t.label,
1694
+ category: t.category,
1695
+ default: t.is_default ? "\u2713" : ""
1696
+ })));
1697
+ await client.close();
1698
+ });
1699
+ cmd.command("add <type_name>").requiredOption("--label <label>", "Display label").requiredOption("--category <category>", "Category").option("--description <desc>", "Description").action(async (typeName, opts) => {
1700
+ const client = await getClient();
1701
+ const result = await client.call("activity_type_add", {
1702
+ type_name: typeName,
1703
+ label: opts.label,
1704
+ category: opts.category,
1705
+ description: opts.description
1706
+ });
1707
+ const data = JSON.parse(result);
1708
+ console.log(`
1709
+ Added activity type: ${data.activity_type.type_name}
1710
+ `);
1711
+ await client.close();
1712
+ });
1713
+ cmd.command("remove <type_name>").description("Remove a custom activity type (cannot remove defaults)").action(async (typeName) => {
1714
+ const client = await getClient();
1715
+ try {
1716
+ await client.call("activity_type_remove", { type_name: typeName });
1717
+ console.log(`
1718
+ Removed activity type: ${typeName}
1719
+ `);
1720
+ } catch (err) {
1721
+ console.error(`
1722
+ Error: ${err instanceof Error ? err.message : err}
1723
+ `);
1724
+ }
1725
+ await client.close();
1726
+ });
1727
+ return cmd;
1728
+ }
1729
+
1730
+ // src/commands/context-types.ts
1731
+ import { Command as Command23 } from "commander";
1732
+ function contextTypesCommand() {
1733
+ const cmd = new Command23("context-types").description("Manage context type registry");
1734
+ cmd.command("list").action(async () => {
1735
+ const client = await getClient();
1736
+ const result = await client.call("context_type_list", {});
1737
+ const data = JSON.parse(result);
1738
+ if (data.context_types?.length === 0) {
1739
+ console.log("No context types found.");
1740
+ return;
1741
+ }
1742
+ console.table(data.context_types?.map((t) => ({
1743
+ type_name: t.type_name,
1744
+ label: t.label,
1745
+ description: (t.description ?? "").slice(0, 60),
1746
+ default: t.is_default ? "\u2713" : ""
1747
+ })));
1748
+ await client.close();
1749
+ });
1750
+ cmd.command("add <type_name>").requiredOption("--label <label>", "Display label").option("--description <desc>", "Description").action(async (typeName, opts) => {
1751
+ const client = await getClient();
1752
+ const result = await client.call("context_type_add", {
1753
+ type_name: typeName,
1754
+ label: opts.label,
1755
+ description: opts.description
1756
+ });
1757
+ const data = JSON.parse(result);
1758
+ console.log(`
1759
+ Added context type: ${data.context_type.type_name}
1760
+ `);
1761
+ await client.close();
1762
+ });
1763
+ cmd.command("remove <type_name>").description("Remove a custom context type (cannot remove defaults)").action(async (typeName) => {
1764
+ const client = await getClient();
1765
+ try {
1766
+ await client.call("context_type_remove", { type_name: typeName });
1767
+ console.log(`
1768
+ Removed context type: ${typeName}
1769
+ `);
1770
+ } catch (err) {
1771
+ console.error(`
1772
+ Error: ${err instanceof Error ? err.message : err}
1773
+ `);
1774
+ }
1775
+ await client.close();
1776
+ });
1777
+ return cmd;
1778
+ }
1779
+
1780
+ // src/commands/briefing.ts
1781
+ import { Command as Command24 } from "commander";
1782
+ function briefingCommand() {
1783
+ const cmd = new Command24("briefing").description("Get a unified briefing for any CRM object \u2014 everything you need before taking action").argument("<subject>", "Subject as type:UUID (e.g. contact:550e8400-...)").option("--format <fmt>", "Output format (json or text)", "text").option("--since <duration>", "Filter activities by duration (e.g. 7d, 24h)").option("--context-types <types>", "Comma-separated context types to include").option("--include-stale", "Include superseded context entries").action(async (subject, opts) => {
1784
+ const [subjectType, subjectId] = subject.split(":");
1785
+ if (!subjectType || !subjectId) {
1786
+ console.error("Subject must be in format type:UUID (e.g. contact:550e8400-...)");
1787
+ process.exit(1);
1788
+ }
1789
+ const client = await getClient();
1790
+ const result = await client.call("briefing_get", {
1791
+ subject_type: subjectType,
1792
+ subject_id: subjectId,
1793
+ format: opts.format,
1794
+ since: opts.since,
1795
+ context_types: opts.contextTypes ? opts.contextTypes.split(",") : void 0,
1796
+ include_stale: opts.includeStale ?? false
1797
+ });
1798
+ const data = JSON.parse(result);
1799
+ if (opts.format === "text" && data.briefing_text) {
1800
+ console.log(data.briefing_text);
1801
+ } else {
1802
+ console.log(JSON.stringify(data, null, 2));
1803
+ }
1804
+ await client.close();
1805
+ });
1806
+ return cmd;
1807
+ }
1808
+
1809
+ // src/commands/auth.ts
1810
+ import { Command as Command25 } from "commander";
1811
+ function authCommand() {
1812
+ const cmd = new Command25("auth").description("Authenticate against a CRMy server");
1813
+ cmd.command("setup").description("Configure the CRMy server URL").argument("[url]", "Server URL (e.g. http://localhost:3000)").action(async (urlArg) => {
1814
+ let serverUrl = urlArg;
1815
+ if (!serverUrl) {
1816
+ const { default: inquirer } = await import("inquirer");
1817
+ const answers = await inquirer.prompt([
1818
+ {
1819
+ type: "input",
1820
+ name: "serverUrl",
1821
+ message: "CRMy server URL:",
1822
+ default: "http://localhost:3000"
1823
+ }
1824
+ ]);
1825
+ serverUrl = answers.serverUrl;
1826
+ }
1827
+ try {
1828
+ const res = await fetch(`${serverUrl.replace(/\/$/, "")}/health`);
1829
+ if (!res.ok) {
1830
+ console.error(`Server at ${serverUrl} returned ${res.status}`);
1831
+ process.exit(1);
1832
+ }
1833
+ const data = await res.json();
1834
+ console.log(`
1835
+ Connected to CRMy ${data.version} at ${serverUrl}
1836
+ `);
1837
+ console.log(" Run `crmy login` to authenticate.\n");
1838
+ const existing = loadAuthState();
1839
+ if (existing) {
1840
+ saveAuthState({ ...existing, serverUrl });
1841
+ } else {
1842
+ saveAuthState({
1843
+ serverUrl,
1844
+ token: "",
1845
+ user: { id: "", email: "", name: "", role: "", tenant_id: "" }
1846
+ });
1847
+ }
1848
+ } catch (err) {
1849
+ console.error(`Could not connect to ${serverUrl}: ${err instanceof Error ? err.message : err}`);
1850
+ process.exit(1);
1851
+ }
1852
+ });
1853
+ cmd.command("login").description("Sign in with email and password").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(async (opts) => {
1854
+ const serverUrl = resolveServerUrl();
1855
+ if (!serverUrl) {
1856
+ console.error("No server configured. Run `crmy auth setup` first.");
1857
+ process.exit(1);
1858
+ }
1859
+ let email = opts.email;
1860
+ let password = opts.password;
1861
+ if (!email || !password) {
1862
+ const { default: inquirer } = await import("inquirer");
1863
+ const answers = await inquirer.prompt([
1864
+ ...email ? [] : [{ type: "input", name: "email", message: "Email:" }],
1865
+ ...password ? [] : [{ type: "password", name: "password", message: "Password:", mask: "*" }]
1866
+ ]);
1867
+ email = email ?? answers.email;
1868
+ password = password ?? answers.password;
1869
+ }
1870
+ try {
1871
+ const res = await fetch(`${serverUrl.replace(/\/$/, "")}/auth/login`, {
1872
+ method: "POST",
1873
+ headers: { "Content-Type": "application/json" },
1874
+ body: JSON.stringify({ email, password })
1875
+ });
1876
+ if (!res.ok) {
1877
+ const body = await res.json().catch(() => ({ detail: res.statusText }));
1878
+ console.error(`Login failed: ${body.detail ?? res.statusText}`);
1879
+ process.exit(1);
1880
+ }
1881
+ const data = await res.json();
1882
+ const payloadB64 = data.token.split(".")[1];
1883
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
1884
+ const expiresAt = payload.exp ? new Date(payload.exp * 1e3).toISOString() : void 0;
1885
+ saveAuthState({
1886
+ serverUrl,
1887
+ token: data.token,
1888
+ user: data.user,
1889
+ expiresAt
1890
+ });
1891
+ console.log(`
1892
+ Logged in as ${data.user.name} (${data.user.email})`);
1893
+ console.log(` Role: ${data.user.role}`);
1894
+ if (expiresAt) {
1895
+ console.log(` Token expires: ${new Date(expiresAt).toLocaleString()}`);
1896
+ }
1897
+ console.log();
1898
+ } catch (err) {
1899
+ console.error(`Login failed: ${err instanceof Error ? err.message : err}`);
1900
+ process.exit(1);
1901
+ }
1902
+ });
1903
+ cmd.command("status").description("Show current authentication status").action(() => {
1904
+ const auth = loadAuthState();
1905
+ if (!auth || !auth.token) {
1906
+ console.log("\n Not authenticated. Run `crmy login` to sign in.\n");
1907
+ return;
1908
+ }
1909
+ console.log(`
1910
+ Server: ${auth.serverUrl}`);
1911
+ console.log(` User: ${auth.user.name} (${auth.user.email})`);
1912
+ console.log(` Role: ${auth.user.role}`);
1913
+ if (auth.expiresAt) {
1914
+ const expires = new Date(auth.expiresAt);
1915
+ const remaining = expires.getTime() - Date.now();
1916
+ if (remaining <= 0) {
1917
+ console.log(" Token: Expired \u2014 run `crmy login` to re-authenticate");
1918
+ } else {
1919
+ const mins = Math.floor(remaining / 6e4);
1920
+ console.log(` Token: Valid (${mins}m remaining)`);
1921
+ }
1922
+ }
1923
+ console.log();
1924
+ });
1925
+ cmd.command("logout").description("Clear stored credentials").action(() => {
1926
+ clearAuthState();
1927
+ console.log("\n Logged out. Credentials cleared.\n");
1928
+ });
1929
+ return cmd;
1930
+ }
1931
+
1932
+ // src/commands/help.ts
1933
+ import { Command as Command26 } from "commander";
1934
+ var HELP_TEXT = `
1935
+ CRMy \u2014 The agent-first open source CRM
1936
+
1937
+ Usage: crmy <command> [options]
1938
+
1939
+ Setup & Auth
1940
+ init Initialize crmy: configure database, run migrations, create user
1941
+ login Sign in to a CRMy server (shortcut for crmy auth login)
1942
+ auth Manage authentication (login, logout, whoami)
1943
+ config View and update local configuration
1944
+ migrate Run database migrations
1945
+
1946
+ Server
1947
+ server Start the CRMy server
1948
+ mcp Start the MCP stdio server for Claude Code
1949
+
1950
+ CRM Data
1951
+ contacts Manage contacts (list, get, create, update, delete)
1952
+ accounts Manage accounts (list, get, create, update, delete)
1953
+ opps Manage opportunities (list, get, create, update, delete)
1954
+ pipeline View and manage the sales pipeline
1955
+ notes Manage notes on CRM objects
1956
+ custom-fields Manage custom field definitions
1957
+ search Search across contacts, accounts, and opportunities
1958
+
1959
+ Automation
1960
+ workflows Manage automation workflows
1961
+ events View the event log
1962
+ webhooks Manage webhook subscriptions
1963
+ emails Send and manage emails
1964
+ hitl Human-in-the-loop approval queue
1965
+
1966
+ Resources
1967
+ use-cases Browse example use cases and templates
1968
+
1969
+ Options
1970
+ -V, --version Output the version number
1971
+ -h, --help Display help for a command
1972
+
1973
+ Examples
1974
+ $ crmy init Set up a new CRMy instance
1975
+ $ crmy server Start the server on :3000
1976
+ $ crmy contacts list List all contacts
1977
+ $ crmy opps create Create a new opportunity
1978
+ $ crmy mcp Start MCP server for Claude Code
1979
+
1980
+ Run crmy <command> --help for detailed usage of any command.
1981
+ `;
1982
+ function helpCommand() {
1983
+ return new Command26("help").description("Show detailed help and list all available commands").argument("[command]", "Show help for a specific command").allowExcessArguments(true).action(async (commandName, _opts, cmd) => {
1984
+ if (commandName) {
1985
+ const root = cmd.parent;
1986
+ if (root) {
1987
+ const sub = root.commands.find(
1988
+ (c) => c.name() === commandName
1989
+ );
1990
+ if (sub) {
1991
+ sub.outputHelp();
1992
+ return;
1993
+ }
1994
+ }
1995
+ console.error(` Unknown command: ${commandName}
1996
+ `);
1997
+ console.error(` Run crmy help to see all available commands.`);
1998
+ process.exitCode = 1;
1999
+ return;
2000
+ }
2001
+ console.log(HELP_TEXT);
2002
+ });
2003
+ }
2004
+
2005
+ // src/index.ts
2006
+ var program = new Command27();
2007
+ program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version("0.5.0");
2008
+ program.addCommand(authCommand());
2009
+ program.addCommand(initCommand());
2010
+ program.addCommand(serverCommand());
2011
+ program.addCommand(mcpCommand());
2012
+ program.addCommand(contactsCommand());
2013
+ program.addCommand(accountsCommand());
2014
+ program.addCommand(oppsCommand());
2015
+ program.addCommand(pipelineCommand());
2016
+ program.addCommand(searchCommand());
2017
+ program.addCommand(hitlCommand());
2018
+ program.addCommand(eventsCommand());
2019
+ program.addCommand(configCommand());
2020
+ program.addCommand(migrateCommand());
2021
+ program.addCommand(useCasesCommand());
2022
+ program.addCommand(webhooksCommand());
2023
+ program.addCommand(emailsCommand());
2024
+ program.addCommand(customFieldsCommand());
2025
+ program.addCommand(notesCommand());
2026
+ program.addCommand(workflowsCommand());
2027
+ program.addCommand(actorsCommand());
2028
+ program.addCommand(assignmentsCommand());
2029
+ program.addCommand(contextCommand());
2030
+ program.addCommand(activityTypesCommand());
2031
+ program.addCommand(contextTypesCommand());
2032
+ program.addCommand(briefingCommand());
2033
+ program.addCommand(helpCommand());
2034
+ program.command("login").description("Sign in to a CRMy server (shortcut for `crmy auth login`)").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(async (opts) => {
2035
+ const args = ["auth", "login"];
2036
+ if (opts.email) args.push("-e", opts.email);
2037
+ if (opts.password) args.push("-p", opts.password);
2038
+ await program.parseAsync(["node", "crmy", ...args]);
2039
+ });
2040
+ program.parse();