@hasna/contacts 0.2.9 → 0.3.1
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/cli/index.js +903 -26
- package/dist/db/applications.d.ts +10 -0
- package/dist/db/applications.d.ts.map +1 -0
- package/dist/db/companies.d.ts +4 -0
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contact-tasks.d.ts +19 -0
- package/dist/db/contact-tasks.d.ts.map +1 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/notes.d.ts +2 -1
- package/dist/db/notes.d.ts.map +1 -1
- package/dist/db/org-members.d.ts +9 -0
- package/dist/db/org-members.d.ts.map +1 -0
- package/dist/db/relationships.d.ts +11 -0
- package/dist/db/relationships.d.ts.map +1 -1
- package/dist/db/vendor-comms.d.ts +15 -0
- package/dist/db/vendor-comms.d.ts.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +771 -4
- package/dist/mcp/index.js +1104 -18
- package/dist/server/index.js +102 -4
- package/dist/types/index.d.ts +204 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
function __accessProp(key) {
|
|
9
|
-
return this[key];
|
|
10
|
-
}
|
|
11
|
-
var __toESMCache_node;
|
|
12
|
-
var __toESMCache_esm;
|
|
13
8
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
-
var canCache = mod != null && typeof mod === "object";
|
|
15
|
-
if (canCache) {
|
|
16
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
-
var cached = cache.get(mod);
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __returnValue = (v) => v;
|
|
35
|
-
function __exportSetter(name, newValue) {
|
|
36
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -2387,6 +2369,85 @@ var init_database = __esm(() => {
|
|
|
2387
2369
|
);
|
|
2388
2370
|
|
|
2389
2371
|
CREATE INDEX IF NOT EXISTS idx_contact_notes_contact ON contact_notes(contact_id);
|
|
2372
|
+
`,
|
|
2373
|
+
`
|
|
2374
|
+
ALTER TABLE companies ADD COLUMN is_owned_entity INTEGER NOT NULL DEFAULT 0;
|
|
2375
|
+
ALTER TABLE companies ADD COLUMN entity_type TEXT CHECK(entity_type IN ('operating','holding','dissolved','nonprofit','trust','branch','other'));
|
|
2376
|
+
|
|
2377
|
+
ALTER TABLE company_relationships ADD COLUMN start_date TEXT;
|
|
2378
|
+
ALTER TABLE company_relationships ADD COLUMN end_date TEXT;
|
|
2379
|
+
ALTER TABLE company_relationships ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0;
|
|
2380
|
+
ALTER TABLE company_relationships ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','inactive','ended'));
|
|
2381
|
+
|
|
2382
|
+
CREATE TABLE IF NOT EXISTS org_members (
|
|
2383
|
+
id TEXT PRIMARY KEY,
|
|
2384
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
2385
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2386
|
+
title TEXT,
|
|
2387
|
+
specialization TEXT,
|
|
2388
|
+
office_phone TEXT,
|
|
2389
|
+
response_sla_hours INTEGER,
|
|
2390
|
+
notes TEXT,
|
|
2391
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2392
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2393
|
+
UNIQUE(company_id, contact_id)
|
|
2394
|
+
);
|
|
2395
|
+
|
|
2396
|
+
CREATE TABLE IF NOT EXISTS vendor_communications (
|
|
2397
|
+
id TEXT PRIMARY KEY,
|
|
2398
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
2399
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
|
|
2400
|
+
comm_date TEXT NOT NULL,
|
|
2401
|
+
type TEXT NOT NULL DEFAULT 'email' CHECK(type IN ('email','call','meeting','invoice_request','invoice_received','payment','dispute','other')),
|
|
2402
|
+
direction TEXT NOT NULL DEFAULT 'outbound' CHECK(direction IN ('inbound','outbound')),
|
|
2403
|
+
subject TEXT,
|
|
2404
|
+
body TEXT,
|
|
2405
|
+
status TEXT NOT NULL DEFAULT 'sent' CHECK(status IN ('sent','awaiting_response','responded','no_response','resolved')),
|
|
2406
|
+
invoice_amount REAL,
|
|
2407
|
+
invoice_currency TEXT,
|
|
2408
|
+
invoice_ref TEXT,
|
|
2409
|
+
follow_up_date TEXT,
|
|
2410
|
+
follow_up_done INTEGER NOT NULL DEFAULT 0,
|
|
2411
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2412
|
+
);
|
|
2413
|
+
|
|
2414
|
+
CREATE TABLE IF NOT EXISTS contact_tasks (
|
|
2415
|
+
id TEXT PRIMARY KEY,
|
|
2416
|
+
title TEXT NOT NULL,
|
|
2417
|
+
description TEXT,
|
|
2418
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2419
|
+
assigned_by TEXT,
|
|
2420
|
+
deadline TEXT,
|
|
2421
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','awaiting_response','in_progress','completed','cancelled','escalated')),
|
|
2422
|
+
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low','medium','high','critical')),
|
|
2423
|
+
entity_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
2424
|
+
linked_todos_task_id TEXT,
|
|
2425
|
+
escalation_rules TEXT NOT NULL DEFAULT '[]',
|
|
2426
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2427
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2428
|
+
);
|
|
2429
|
+
|
|
2430
|
+
CREATE TABLE IF NOT EXISTS applications (
|
|
2431
|
+
id TEXT PRIMARY KEY,
|
|
2432
|
+
program_name TEXT NOT NULL,
|
|
2433
|
+
provider_company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
2434
|
+
type TEXT NOT NULL DEFAULT 'other' CHECK(type IN ('ai_credits','grant','startup_program','visa','trademark','tax_filing','loan','other')),
|
|
2435
|
+
value_usd REAL,
|
|
2436
|
+
applicant_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
|
|
2437
|
+
primary_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
|
|
2438
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','submitted','pending','approved','rejected','follow_up_needed','expired','cancelled')),
|
|
2439
|
+
submitted_date TEXT,
|
|
2440
|
+
decision_date TEXT,
|
|
2441
|
+
follow_up_date TEXT,
|
|
2442
|
+
notes TEXT,
|
|
2443
|
+
method TEXT CHECK(method IN ('email','form','typeform','hubspot','manual','browser','feathery','other')),
|
|
2444
|
+
form_url TEXT,
|
|
2445
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
2446
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2447
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
|
|
2390
2451
|
`
|
|
2391
2452
|
];
|
|
2392
2453
|
});
|
|
@@ -2450,7 +2511,9 @@ function rowToCompany(row) {
|
|
|
2450
2511
|
...row,
|
|
2451
2512
|
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
2452
2513
|
archived: !!row.archived,
|
|
2453
|
-
project_id: row.project_id ?? null
|
|
2514
|
+
project_id: row.project_id ?? null,
|
|
2515
|
+
is_owned_entity: !!row.is_owned_entity,
|
|
2516
|
+
entity_type: row.entity_type ?? null
|
|
2454
2517
|
};
|
|
2455
2518
|
}
|
|
2456
2519
|
function insertEmails(db, contactId, companyId, emails) {
|
|
@@ -2767,7 +2830,9 @@ function rowToCompany2(row) {
|
|
|
2767
2830
|
...row,
|
|
2768
2831
|
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
2769
2832
|
archived: !!row.archived,
|
|
2770
|
-
project_id: row.project_id ?? null
|
|
2833
|
+
project_id: row.project_id ?? null,
|
|
2834
|
+
is_owned_entity: !!row.is_owned_entity,
|
|
2835
|
+
entity_type: row.entity_type ?? null
|
|
2771
2836
|
};
|
|
2772
2837
|
}
|
|
2773
2838
|
function insertEmails2(db, companyId, emails) {
|
|
@@ -2831,8 +2896,8 @@ function createCompany(input, db) {
|
|
|
2831
2896
|
const d = db || getDatabase();
|
|
2832
2897
|
const id = uuid();
|
|
2833
2898
|
const timestamp = now();
|
|
2834
|
-
d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, created_at, updated_at)
|
|
2835
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2899
|
+
d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, is_owned_entity, entity_type, created_at, updated_at)
|
|
2900
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2836
2901
|
id,
|
|
2837
2902
|
input.name,
|
|
2838
2903
|
input.domain ?? null,
|
|
@@ -2843,6 +2908,8 @@ function createCompany(input, db) {
|
|
|
2843
2908
|
input.founded_year ?? null,
|
|
2844
2909
|
input.notes ?? null,
|
|
2845
2910
|
JSON.stringify(input.custom_fields ?? {}),
|
|
2911
|
+
input.is_owned_entity ? 1 : 0,
|
|
2912
|
+
input.entity_type ?? null,
|
|
2846
2913
|
timestamp,
|
|
2847
2914
|
timestamp
|
|
2848
2915
|
]);
|
|
@@ -2879,6 +2946,7 @@ function listCompanies(opts = {}, db) {
|
|
|
2879
2946
|
tag_id,
|
|
2880
2947
|
project_id,
|
|
2881
2948
|
archived = false,
|
|
2949
|
+
is_owned_entity,
|
|
2882
2950
|
order_by = "name",
|
|
2883
2951
|
order_dir = "asc"
|
|
2884
2952
|
} = opts;
|
|
@@ -2898,6 +2966,10 @@ function listCompanies(opts = {}, db) {
|
|
|
2898
2966
|
conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
|
|
2899
2967
|
params.push(tag_id);
|
|
2900
2968
|
}
|
|
2969
|
+
if (is_owned_entity !== undefined) {
|
|
2970
|
+
conditions.push("co.is_owned_entity = ?");
|
|
2971
|
+
params.push(is_owned_entity ? 1 : 0);
|
|
2972
|
+
}
|
|
2901
2973
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2902
2974
|
const validOrderBy = ["name", "created_at", "updated_at"].includes(order_by) ? order_by : "name";
|
|
2903
2975
|
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
@@ -2953,6 +3025,14 @@ function updateCompany(id, input, db) {
|
|
|
2953
3025
|
setClauses.push("project_id = ?");
|
|
2954
3026
|
params.push(input.project_id);
|
|
2955
3027
|
}
|
|
3028
|
+
if (input.is_owned_entity !== undefined) {
|
|
3029
|
+
setClauses.push("is_owned_entity = ?");
|
|
3030
|
+
params.push(input.is_owned_entity ? 1 : 0);
|
|
3031
|
+
}
|
|
3032
|
+
if ("entity_type" in input && input.entity_type !== undefined) {
|
|
3033
|
+
setClauses.push("entity_type = ?");
|
|
3034
|
+
params.push(input.entity_type);
|
|
3035
|
+
}
|
|
2956
3036
|
params.push(id);
|
|
2957
3037
|
d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
2958
3038
|
logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
|
|
@@ -3747,9 +3827,374 @@ var {
|
|
|
3747
3827
|
// src/cli/index.tsx
|
|
3748
3828
|
init_contacts();
|
|
3749
3829
|
init_companies();
|
|
3750
|
-
init_tags();
|
|
3751
3830
|
import chalk from "chalk";
|
|
3752
3831
|
|
|
3832
|
+
// src/db/relationships.ts
|
|
3833
|
+
init_types();
|
|
3834
|
+
init_database();
|
|
3835
|
+
function rowToCompanyRelationship(row) {
|
|
3836
|
+
return {
|
|
3837
|
+
...row,
|
|
3838
|
+
relationship_type: row.relationship_type,
|
|
3839
|
+
start_date: row.start_date ?? null,
|
|
3840
|
+
end_date: row.end_date ?? null,
|
|
3841
|
+
is_primary: !!row.is_primary,
|
|
3842
|
+
status: row.status ?? "active"
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
function createCompanyRelationship(input, db) {
|
|
3846
|
+
const d = db || getDatabase();
|
|
3847
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
|
|
3848
|
+
if (!contact)
|
|
3849
|
+
throw new ContactNotFoundError(input.contact_id);
|
|
3850
|
+
const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
|
|
3851
|
+
if (!company)
|
|
3852
|
+
throw new Error(`Company ${input.company_id} not found`);
|
|
3853
|
+
const id = uuid();
|
|
3854
|
+
d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3855
|
+
id,
|
|
3856
|
+
input.contact_id,
|
|
3857
|
+
input.company_id,
|
|
3858
|
+
input.relationship_type,
|
|
3859
|
+
input.notes ?? null,
|
|
3860
|
+
input.start_date ?? null,
|
|
3861
|
+
input.end_date ?? null,
|
|
3862
|
+
input.is_primary ? 1 : 0,
|
|
3863
|
+
input.status ?? "active"
|
|
3864
|
+
]);
|
|
3865
|
+
return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
|
|
3866
|
+
}
|
|
3867
|
+
function listCompanyRelationships(opts = {}, db) {
|
|
3868
|
+
const d = db || getDatabase();
|
|
3869
|
+
const conditions = [];
|
|
3870
|
+
const params = [];
|
|
3871
|
+
if (opts.contact_id) {
|
|
3872
|
+
conditions.push("contact_id = ?");
|
|
3873
|
+
params.push(opts.contact_id);
|
|
3874
|
+
}
|
|
3875
|
+
if (opts.company_id) {
|
|
3876
|
+
conditions.push("company_id = ?");
|
|
3877
|
+
params.push(opts.company_id);
|
|
3878
|
+
}
|
|
3879
|
+
if (opts.relationship_type) {
|
|
3880
|
+
conditions.push("relationship_type = ?");
|
|
3881
|
+
params.push(opts.relationship_type);
|
|
3882
|
+
}
|
|
3883
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3884
|
+
const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
|
|
3885
|
+
return rows.map(rowToCompanyRelationship);
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// src/db/vendor-comms.ts
|
|
3889
|
+
init_database();
|
|
3890
|
+
function rowToVendorComm(row) {
|
|
3891
|
+
return {
|
|
3892
|
+
id: row.id,
|
|
3893
|
+
company_id: row.company_id,
|
|
3894
|
+
contact_id: row.contact_id,
|
|
3895
|
+
comm_date: row.comm_date,
|
|
3896
|
+
type: row.type,
|
|
3897
|
+
direction: row.direction,
|
|
3898
|
+
subject: row.subject,
|
|
3899
|
+
body: row.body,
|
|
3900
|
+
status: row.status,
|
|
3901
|
+
invoice_amount: row.invoice_amount,
|
|
3902
|
+
invoice_currency: row.invoice_currency,
|
|
3903
|
+
invoice_ref: row.invoice_ref,
|
|
3904
|
+
follow_up_date: row.follow_up_date,
|
|
3905
|
+
follow_up_done: !!row.follow_up_done,
|
|
3906
|
+
created_at: row.created_at
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
function logVendorCommunication(input, db) {
|
|
3910
|
+
const d = db || getDatabase();
|
|
3911
|
+
const id = uuid();
|
|
3912
|
+
d.run(`INSERT INTO vendor_communications
|
|
3913
|
+
(id, company_id, contact_id, comm_date, type, direction, subject, body, status,
|
|
3914
|
+
invoice_amount, invoice_currency, invoice_ref, follow_up_date, follow_up_done)
|
|
3915
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3916
|
+
id,
|
|
3917
|
+
input.company_id,
|
|
3918
|
+
input.contact_id ?? null,
|
|
3919
|
+
input.comm_date,
|
|
3920
|
+
input.type ?? "email",
|
|
3921
|
+
input.direction ?? "outbound",
|
|
3922
|
+
input.subject ?? null,
|
|
3923
|
+
input.body ?? null,
|
|
3924
|
+
input.status ?? "sent",
|
|
3925
|
+
input.invoice_amount ?? null,
|
|
3926
|
+
input.invoice_currency ?? null,
|
|
3927
|
+
input.invoice_ref ?? null,
|
|
3928
|
+
input.follow_up_date ?? null,
|
|
3929
|
+
input.follow_up_done ? 1 : 0
|
|
3930
|
+
]);
|
|
3931
|
+
return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
|
|
3932
|
+
}
|
|
3933
|
+
function listVendorCommunications(companyId, opts = {}, db) {
|
|
3934
|
+
const d = db || getDatabase();
|
|
3935
|
+
const conditions = ["company_id = ?"];
|
|
3936
|
+
const params = [companyId];
|
|
3937
|
+
if (opts.type) {
|
|
3938
|
+
conditions.push("type = ?");
|
|
3939
|
+
params.push(opts.type);
|
|
3940
|
+
}
|
|
3941
|
+
if (opts.status) {
|
|
3942
|
+
conditions.push("status = ?");
|
|
3943
|
+
params.push(opts.status);
|
|
3944
|
+
}
|
|
3945
|
+
if (opts.direction) {
|
|
3946
|
+
conditions.push("direction = ?");
|
|
3947
|
+
params.push(opts.direction);
|
|
3948
|
+
}
|
|
3949
|
+
const where = conditions.join(" AND ");
|
|
3950
|
+
const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
|
|
3951
|
+
return rows.map(rowToVendorComm);
|
|
3952
|
+
}
|
|
3953
|
+
function listPendingFollowUps(db) {
|
|
3954
|
+
const d = db || getDatabase();
|
|
3955
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
3956
|
+
const rows = d.query(`SELECT * FROM vendor_communications
|
|
3957
|
+
WHERE follow_up_date <= ? AND follow_up_done = 0
|
|
3958
|
+
ORDER BY follow_up_date ASC`).all(today);
|
|
3959
|
+
return rows.map(rowToVendorComm);
|
|
3960
|
+
}
|
|
3961
|
+
function listMissingInvoices(db) {
|
|
3962
|
+
const d = db || getDatabase();
|
|
3963
|
+
const rows = d.query(`SELECT * FROM vendor_communications
|
|
3964
|
+
WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
|
|
3965
|
+
ORDER BY comm_date ASC`).all();
|
|
3966
|
+
return rows.map(rowToVendorComm);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// src/db/contact-tasks.ts
|
|
3970
|
+
init_database();
|
|
3971
|
+
function rowToContactTask(row) {
|
|
3972
|
+
return {
|
|
3973
|
+
id: row.id,
|
|
3974
|
+
title: row.title,
|
|
3975
|
+
description: row.description,
|
|
3976
|
+
contact_id: row.contact_id,
|
|
3977
|
+
assigned_by: row.assigned_by,
|
|
3978
|
+
deadline: row.deadline,
|
|
3979
|
+
status: row.status,
|
|
3980
|
+
priority: row.priority,
|
|
3981
|
+
entity_id: row.entity_id,
|
|
3982
|
+
linked_todos_task_id: row.linked_todos_task_id,
|
|
3983
|
+
escalation_rules: JSON.parse(row.escalation_rules || "[]"),
|
|
3984
|
+
created_at: row.created_at,
|
|
3985
|
+
updated_at: row.updated_at
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
function createContactTask(input, db) {
|
|
3989
|
+
const d = db || getDatabase();
|
|
3990
|
+
const id = uuid();
|
|
3991
|
+
const timestamp = now();
|
|
3992
|
+
d.run(`INSERT INTO contact_tasks
|
|
3993
|
+
(id, title, description, contact_id, assigned_by, deadline, status, priority,
|
|
3994
|
+
entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
|
|
3995
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3996
|
+
id,
|
|
3997
|
+
input.title,
|
|
3998
|
+
input.description ?? null,
|
|
3999
|
+
input.contact_id,
|
|
4000
|
+
input.assigned_by ?? null,
|
|
4001
|
+
input.deadline ?? null,
|
|
4002
|
+
input.status ?? "pending",
|
|
4003
|
+
input.priority ?? "medium",
|
|
4004
|
+
input.entity_id ?? null,
|
|
4005
|
+
input.linked_todos_task_id ?? null,
|
|
4006
|
+
JSON.stringify(input.escalation_rules ?? []),
|
|
4007
|
+
timestamp,
|
|
4008
|
+
timestamp
|
|
4009
|
+
]);
|
|
4010
|
+
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
4011
|
+
}
|
|
4012
|
+
function listContactTasks(opts = {}, db) {
|
|
4013
|
+
const d = db || getDatabase();
|
|
4014
|
+
const conditions = [];
|
|
4015
|
+
const params = [];
|
|
4016
|
+
if (opts.contact_id) {
|
|
4017
|
+
conditions.push("contact_id = ?");
|
|
4018
|
+
params.push(opts.contact_id);
|
|
4019
|
+
}
|
|
4020
|
+
if (opts.entity_id) {
|
|
4021
|
+
conditions.push("entity_id = ?");
|
|
4022
|
+
params.push(opts.entity_id);
|
|
4023
|
+
}
|
|
4024
|
+
if (opts.status) {
|
|
4025
|
+
conditions.push("status = ?");
|
|
4026
|
+
params.push(opts.status);
|
|
4027
|
+
}
|
|
4028
|
+
if (opts.priority) {
|
|
4029
|
+
conditions.push("priority = ?");
|
|
4030
|
+
params.push(opts.priority);
|
|
4031
|
+
}
|
|
4032
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4033
|
+
const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
|
|
4034
|
+
return rows.map(rowToContactTask);
|
|
4035
|
+
}
|
|
4036
|
+
function updateContactTask(id, input, db) {
|
|
4037
|
+
const d = db || getDatabase();
|
|
4038
|
+
const setClauses = ["updated_at = ?"];
|
|
4039
|
+
const params = [now()];
|
|
4040
|
+
if (input.title !== undefined) {
|
|
4041
|
+
setClauses.push("title = ?");
|
|
4042
|
+
params.push(input.title);
|
|
4043
|
+
}
|
|
4044
|
+
if ("description" in input) {
|
|
4045
|
+
setClauses.push("description = ?");
|
|
4046
|
+
params.push(input.description ?? null);
|
|
4047
|
+
}
|
|
4048
|
+
if ("assigned_by" in input) {
|
|
4049
|
+
setClauses.push("assigned_by = ?");
|
|
4050
|
+
params.push(input.assigned_by ?? null);
|
|
4051
|
+
}
|
|
4052
|
+
if ("deadline" in input) {
|
|
4053
|
+
setClauses.push("deadline = ?");
|
|
4054
|
+
params.push(input.deadline ?? null);
|
|
4055
|
+
}
|
|
4056
|
+
if (input.status !== undefined) {
|
|
4057
|
+
setClauses.push("status = ?");
|
|
4058
|
+
params.push(input.status);
|
|
4059
|
+
}
|
|
4060
|
+
if (input.priority !== undefined) {
|
|
4061
|
+
setClauses.push("priority = ?");
|
|
4062
|
+
params.push(input.priority);
|
|
4063
|
+
}
|
|
4064
|
+
if ("entity_id" in input) {
|
|
4065
|
+
setClauses.push("entity_id = ?");
|
|
4066
|
+
params.push(input.entity_id ?? null);
|
|
4067
|
+
}
|
|
4068
|
+
if ("linked_todos_task_id" in input) {
|
|
4069
|
+
setClauses.push("linked_todos_task_id = ?");
|
|
4070
|
+
params.push(input.linked_todos_task_id ?? null);
|
|
4071
|
+
}
|
|
4072
|
+
if (input.escalation_rules !== undefined) {
|
|
4073
|
+
setClauses.push("escalation_rules = ?");
|
|
4074
|
+
params.push(JSON.stringify(input.escalation_rules));
|
|
4075
|
+
}
|
|
4076
|
+
params.push(id);
|
|
4077
|
+
d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
4078
|
+
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
4079
|
+
}
|
|
4080
|
+
function listOverdueTasks(db) {
|
|
4081
|
+
const d = db || getDatabase();
|
|
4082
|
+
const now_iso = new Date().toISOString();
|
|
4083
|
+
const rows = d.query(`SELECT * FROM contact_tasks
|
|
4084
|
+
WHERE deadline < ? AND status NOT IN ('completed','cancelled')
|
|
4085
|
+
ORDER BY deadline ASC`).all(now_iso);
|
|
4086
|
+
return rows.map(rowToContactTask);
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
// src/db/applications.ts
|
|
4090
|
+
init_database();
|
|
4091
|
+
function rowToApplication(row) {
|
|
4092
|
+
return {
|
|
4093
|
+
id: row.id,
|
|
4094
|
+
program_name: row.program_name,
|
|
4095
|
+
provider_company_id: row.provider_company_id,
|
|
4096
|
+
type: row.type,
|
|
4097
|
+
value_usd: row.value_usd,
|
|
4098
|
+
applicant_contact_id: row.applicant_contact_id,
|
|
4099
|
+
primary_contact_id: row.primary_contact_id,
|
|
4100
|
+
status: row.status,
|
|
4101
|
+
submitted_date: row.submitted_date,
|
|
4102
|
+
decision_date: row.decision_date,
|
|
4103
|
+
follow_up_date: row.follow_up_date,
|
|
4104
|
+
notes: row.notes,
|
|
4105
|
+
method: row.method ?? null,
|
|
4106
|
+
form_url: row.form_url,
|
|
4107
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
4108
|
+
created_at: row.created_at,
|
|
4109
|
+
updated_at: row.updated_at
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
function createApplication(input, db) {
|
|
4113
|
+
const d = db || getDatabase();
|
|
4114
|
+
const id = uuid();
|
|
4115
|
+
const timestamp = now();
|
|
4116
|
+
d.run(`INSERT INTO applications
|
|
4117
|
+
(id, program_name, provider_company_id, type, value_usd, applicant_contact_id, primary_contact_id,
|
|
4118
|
+
status, submitted_date, decision_date, follow_up_date, notes, method, form_url, metadata, created_at, updated_at)
|
|
4119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4120
|
+
id,
|
|
4121
|
+
input.program_name,
|
|
4122
|
+
input.provider_company_id ?? null,
|
|
4123
|
+
input.type ?? "other",
|
|
4124
|
+
input.value_usd ?? null,
|
|
4125
|
+
input.applicant_contact_id ?? null,
|
|
4126
|
+
input.primary_contact_id ?? null,
|
|
4127
|
+
input.status ?? "draft",
|
|
4128
|
+
input.submitted_date ?? null,
|
|
4129
|
+
input.decision_date ?? null,
|
|
4130
|
+
input.follow_up_date ?? null,
|
|
4131
|
+
input.notes ?? null,
|
|
4132
|
+
input.method ?? null,
|
|
4133
|
+
input.form_url ?? null,
|
|
4134
|
+
JSON.stringify(input.metadata ?? {}),
|
|
4135
|
+
timestamp,
|
|
4136
|
+
timestamp
|
|
4137
|
+
]);
|
|
4138
|
+
return rowToApplication(d.query(`SELECT * FROM applications WHERE id = ?`).get(id));
|
|
4139
|
+
}
|
|
4140
|
+
function listApplications(opts = {}, db) {
|
|
4141
|
+
const d = db || getDatabase();
|
|
4142
|
+
const conditions = [];
|
|
4143
|
+
const params = [];
|
|
4144
|
+
if (opts.type) {
|
|
4145
|
+
conditions.push("type = ?");
|
|
4146
|
+
params.push(opts.type);
|
|
4147
|
+
}
|
|
4148
|
+
if (opts.status) {
|
|
4149
|
+
conditions.push("status = ?");
|
|
4150
|
+
params.push(opts.status);
|
|
4151
|
+
}
|
|
4152
|
+
if (opts.provider_company_id) {
|
|
4153
|
+
conditions.push("provider_company_id = ?");
|
|
4154
|
+
params.push(opts.provider_company_id);
|
|
4155
|
+
}
|
|
4156
|
+
if (opts.applicant_contact_id) {
|
|
4157
|
+
conditions.push("applicant_contact_id = ?");
|
|
4158
|
+
params.push(opts.applicant_contact_id);
|
|
4159
|
+
}
|
|
4160
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4161
|
+
const rows = d.query(`SELECT * FROM applications ${where} ORDER BY created_at DESC`).all(...params);
|
|
4162
|
+
return rows.map(rowToApplication);
|
|
4163
|
+
}
|
|
4164
|
+
function listFollowUpDue(db) {
|
|
4165
|
+
const d = db || getDatabase();
|
|
4166
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
4167
|
+
const rows = d.query(`SELECT * FROM applications
|
|
4168
|
+
WHERE follow_up_date <= ? AND status = 'follow_up_needed'
|
|
4169
|
+
ORDER BY follow_up_date ASC`).all(today);
|
|
4170
|
+
return rows.map(rowToApplication);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
// src/db/org-members.ts
|
|
4174
|
+
init_database();
|
|
4175
|
+
function rowToOrgMember(row) {
|
|
4176
|
+
return {
|
|
4177
|
+
id: row.id,
|
|
4178
|
+
company_id: row.company_id,
|
|
4179
|
+
contact_id: row.contact_id,
|
|
4180
|
+
title: row.title,
|
|
4181
|
+
specialization: row.specialization,
|
|
4182
|
+
office_phone: row.office_phone,
|
|
4183
|
+
response_sla_hours: row.response_sla_hours,
|
|
4184
|
+
notes: row.notes,
|
|
4185
|
+
created_at: row.created_at,
|
|
4186
|
+
updated_at: row.updated_at
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
4189
|
+
function listOrgMembersForContact(contactId, db) {
|
|
4190
|
+
const d = db || getDatabase();
|
|
4191
|
+
const rows = d.query(`SELECT * FROM org_members WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
|
|
4192
|
+
return rows.map(rowToOrgMember);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
// src/cli/index.tsx
|
|
4196
|
+
init_tags();
|
|
4197
|
+
|
|
3753
4198
|
// src/db/groups.ts
|
|
3754
4199
|
init_database();
|
|
3755
4200
|
function createGroup(db, input) {
|
|
@@ -3939,7 +4384,7 @@ async function confirm(question) {
|
|
|
3939
4384
|
const answer = await prompt(question + " [y/N]");
|
|
3940
4385
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
3941
4386
|
}
|
|
3942
|
-
program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.
|
|
4387
|
+
program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.3.0");
|
|
3943
4388
|
function collect(val, prev) {
|
|
3944
4389
|
return [...prev, val];
|
|
3945
4390
|
}
|
|
@@ -4655,4 +5100,436 @@ complete -F _contacts_completion contacts`);
|
|
|
4655
5100
|
console.error(chalk.red("Supported shells: bash, zsh, fish"));
|
|
4656
5101
|
}
|
|
4657
5102
|
});
|
|
5103
|
+
var entities = program.command("entities").description("Manage your owned legal entities");
|
|
5104
|
+
entities.command("list").description("List all owned legal entities").action(() => {
|
|
5105
|
+
const db = getDatabase();
|
|
5106
|
+
const result = listCompanies({ limit: 200 }, db);
|
|
5107
|
+
const owned = result.companies.filter((c) => c.is_owned_entity);
|
|
5108
|
+
if (!owned.length) {
|
|
5109
|
+
console.log(chalk.yellow("No owned entities found. Add one with: contacts companies add"));
|
|
5110
|
+
return;
|
|
5111
|
+
}
|
|
5112
|
+
console.log();
|
|
5113
|
+
renderTable(["Name", "Type", "Industry", "Description"], owned.map((e) => ({
|
|
5114
|
+
Name: e.name,
|
|
5115
|
+
Type: e.entity_type || "operating",
|
|
5116
|
+
Industry: e.industry || "",
|
|
5117
|
+
Description: e.description || ""
|
|
5118
|
+
})));
|
|
5119
|
+
console.log(chalk.gray(`
|
|
5120
|
+
${owned.length} owned entity/entities
|
|
5121
|
+
`));
|
|
5122
|
+
});
|
|
5123
|
+
entities.command("show <id>").description("Show entity with full team").action((id) => {
|
|
5124
|
+
const db = getDatabase();
|
|
5125
|
+
const company = getCompany(id);
|
|
5126
|
+
if (!company) {
|
|
5127
|
+
console.error(chalk.red(`
|
|
5128
|
+
Entity not found: ${id}
|
|
5129
|
+
`));
|
|
5130
|
+
process.exit(1);
|
|
5131
|
+
}
|
|
5132
|
+
console.log(`
|
|
5133
|
+
` + chalk.bold.blue("\u2501\u2501\u2501 Entity: ") + chalk.bold(company.name) + chalk.bold.blue(" \u2501\u2501\u2501"));
|
|
5134
|
+
console.log();
|
|
5135
|
+
if (company.industry)
|
|
5136
|
+
console.log(chalk.gray(" Industry: ") + company.industry);
|
|
5137
|
+
if (company.description)
|
|
5138
|
+
console.log(chalk.gray(" Description: ") + company.description);
|
|
5139
|
+
console.log(chalk.gray(` ID: ${company.id}`));
|
|
5140
|
+
console.log();
|
|
5141
|
+
const team = listCompanyRelationships({ company_id: id }, db);
|
|
5142
|
+
if (team.length === 0) {
|
|
5143
|
+
console.log(chalk.gray(` No team members assigned.
|
|
5144
|
+
`));
|
|
5145
|
+
return;
|
|
5146
|
+
}
|
|
5147
|
+
console.log(chalk.yellow(` Team (${team.length}):
|
|
5148
|
+
`));
|
|
5149
|
+
const rows = team.map((r) => {
|
|
5150
|
+
let name = r.contact_id;
|
|
5151
|
+
try {
|
|
5152
|
+
const c = getContact(r.contact_id);
|
|
5153
|
+
name = c.display_name;
|
|
5154
|
+
} catch {}
|
|
5155
|
+
return {
|
|
5156
|
+
Contact: name,
|
|
5157
|
+
Role: r.relationship_type,
|
|
5158
|
+
Primary: r.is_primary ? "yes" : "",
|
|
5159
|
+
Status: r.status
|
|
5160
|
+
};
|
|
5161
|
+
});
|
|
5162
|
+
renderTable(["Contact", "Role", "Primary", "Status"], rows);
|
|
5163
|
+
console.log();
|
|
5164
|
+
});
|
|
5165
|
+
entities.command("add-contact <entity-id> <contact-id>").description("Link a contact to an entity with a role").option("--role <role>", "Role (tax_preparer|bank_manager|attorney|registered_agent|accountant|payroll_provider|insurance_broker|advisor|other)", "other").option("--primary", "Mark as primary contact for this role").action((entityId, contactId, opts) => {
|
|
5166
|
+
const db = getDatabase();
|
|
5167
|
+
const contact = getContact(contactId);
|
|
5168
|
+
const company = getCompany(entityId);
|
|
5169
|
+
if (!company) {
|
|
5170
|
+
console.error(chalk.red(`
|
|
5171
|
+
Entity not found: ${entityId}
|
|
5172
|
+
`));
|
|
5173
|
+
process.exit(1);
|
|
5174
|
+
}
|
|
5175
|
+
const rel = createCompanyRelationship({
|
|
5176
|
+
contact_id: contactId,
|
|
5177
|
+
company_id: entityId,
|
|
5178
|
+
relationship_type: opts.role || "other",
|
|
5179
|
+
is_primary: opts.primary || false
|
|
5180
|
+
}, db);
|
|
5181
|
+
console.log(chalk.green(`
|
|
5182
|
+
\u2713 Linked ${contact.display_name} to ${company.name} as ${rel.relationship_type}
|
|
5183
|
+
`));
|
|
5184
|
+
});
|
|
5185
|
+
entities.command("team <id>").description("Show full team matrix for an entity").action((id) => {
|
|
5186
|
+
const db = getDatabase();
|
|
5187
|
+
const company = getCompany(id);
|
|
5188
|
+
if (!company) {
|
|
5189
|
+
console.error(chalk.red(`
|
|
5190
|
+
Entity not found: ${id}
|
|
5191
|
+
`));
|
|
5192
|
+
process.exit(1);
|
|
5193
|
+
}
|
|
5194
|
+
const team = listCompanyRelationships({ company_id: id }, db);
|
|
5195
|
+
if (team.length === 0) {
|
|
5196
|
+
console.log(chalk.yellow(`
|
|
5197
|
+
No team members for ${company.name}.
|
|
5198
|
+
`));
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
console.log(`
|
|
5202
|
+
` + chalk.bold(`Team: ${company.name}`) + `
|
|
5203
|
+
`);
|
|
5204
|
+
const rows = team.map((r) => {
|
|
5205
|
+
let name = r.contact_id;
|
|
5206
|
+
try {
|
|
5207
|
+
const c = getContact(r.contact_id);
|
|
5208
|
+
name = c.display_name;
|
|
5209
|
+
} catch {}
|
|
5210
|
+
return {
|
|
5211
|
+
Contact: name,
|
|
5212
|
+
Role: r.relationship_type,
|
|
5213
|
+
Primary: r.is_primary ? "yes" : "",
|
|
5214
|
+
Status: r.status
|
|
5215
|
+
};
|
|
5216
|
+
});
|
|
5217
|
+
renderTable(["Contact", "Role", "Primary", "Status"], rows);
|
|
5218
|
+
console.log();
|
|
5219
|
+
});
|
|
5220
|
+
program.command("workload <id>").description("Show workload summary for a contact").action((id) => {
|
|
5221
|
+
const db = getDatabase();
|
|
5222
|
+
const contact = getContact(id);
|
|
5223
|
+
const companyRels = listCompanyRelationships({ contact_id: id }, db);
|
|
5224
|
+
const overdue = listOverdueTasks(db).filter((t) => t.contact_id === id);
|
|
5225
|
+
const orgMemberships = listOrgMembersForContact(id, db);
|
|
5226
|
+
console.log(chalk.bold(`
|
|
5227
|
+
${contact.display_name}`));
|
|
5228
|
+
if (contact.job_title)
|
|
5229
|
+
console.log(chalk.gray(` ${contact.job_title}`));
|
|
5230
|
+
console.log();
|
|
5231
|
+
if (companyRels.length) {
|
|
5232
|
+
console.log(chalk.cyan(` Manages/linked to ${companyRels.length} company relationship(s):`));
|
|
5233
|
+
for (const r of companyRels.slice(0, 5)) {
|
|
5234
|
+
console.log(chalk.gray(` \u2022 ${r.relationship_type}${r.notes ? ` \u2014 ${r.notes}` : ""}`));
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
if (orgMemberships.length) {
|
|
5238
|
+
console.log(chalk.cyan(` Org memberships: ${orgMemberships.length}`));
|
|
5239
|
+
}
|
|
5240
|
+
if (overdue.length) {
|
|
5241
|
+
console.log(chalk.red(` \u26A0 Overdue tasks: ${overdue.length}`));
|
|
5242
|
+
} else {
|
|
5243
|
+
console.log(chalk.green(" No overdue tasks"));
|
|
5244
|
+
}
|
|
5245
|
+
if (contact.last_contacted_at) {
|
|
5246
|
+
const days = Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000);
|
|
5247
|
+
const color = days > 30 ? chalk.red : days > 14 ? chalk.yellow : chalk.green;
|
|
5248
|
+
console.log(color(` Last contact: ${days} day(s) ago`));
|
|
5249
|
+
} else {
|
|
5250
|
+
console.log(chalk.gray(" Never contacted"));
|
|
5251
|
+
}
|
|
5252
|
+
console.log();
|
|
5253
|
+
});
|
|
5254
|
+
var vendor = program.command("vendor").description("Track vendor communications and invoices");
|
|
5255
|
+
vendor.command("log <company-id>").description("Log a vendor communication").option("--type <type>", "Type: email|invoice_request|invoice_received|call|payment|dispute|other", "email").option("--subject <subject>", "Subject").option("--body <body>", "Body/notes").option("--amount <n>", "Invoice amount").option("--currency <c>", "Currency", "USD").option("--ref <ref>", "Invoice reference").option("--follow-up <date>", "Follow-up date (YYYY-MM-DD)").option("--status <status>", "Status: sent|awaiting_response|responded|no_response|resolved", "sent").option("--contact <id>", "Contact ID (optional)").option("--direction <dir>", "Direction: inbound|outbound", "outbound").action((companyId, opts) => {
|
|
5256
|
+
const db = getDatabase();
|
|
5257
|
+
const company = getCompany(companyId);
|
|
5258
|
+
if (!company) {
|
|
5259
|
+
console.error(chalk.red(`
|
|
5260
|
+
Company not found: ${companyId}
|
|
5261
|
+
`));
|
|
5262
|
+
process.exit(1);
|
|
5263
|
+
}
|
|
5264
|
+
const comm = logVendorCommunication({
|
|
5265
|
+
company_id: companyId,
|
|
5266
|
+
contact_id: opts.contact,
|
|
5267
|
+
comm_date: new Date().toISOString().slice(0, 10),
|
|
5268
|
+
type: opts.type || "email",
|
|
5269
|
+
direction: opts.direction || "outbound",
|
|
5270
|
+
subject: opts.subject,
|
|
5271
|
+
body: opts.body,
|
|
5272
|
+
status: opts.status || "sent",
|
|
5273
|
+
invoice_amount: opts.amount ? parseFloat(opts.amount) : undefined,
|
|
5274
|
+
invoice_currency: opts.currency,
|
|
5275
|
+
invoice_ref: opts.ref,
|
|
5276
|
+
follow_up_date: opts.followUp
|
|
5277
|
+
}, db);
|
|
5278
|
+
console.log(chalk.green(`
|
|
5279
|
+
\u2713 Logged ${comm.type} communication with ${company.name} (${comm.id})
|
|
5280
|
+
`));
|
|
5281
|
+
});
|
|
5282
|
+
vendor.command("missing-invoices").description("Show all invoice requests with no response").action(() => {
|
|
5283
|
+
const db = getDatabase();
|
|
5284
|
+
const missing = listMissingInvoices(db);
|
|
5285
|
+
if (!missing.length) {
|
|
5286
|
+
console.log(chalk.green(`
|
|
5287
|
+
No missing invoices.
|
|
5288
|
+
`));
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
5291
|
+
console.log(chalk.yellow(`
|
|
5292
|
+
${missing.length} missing invoice(s):
|
|
5293
|
+
`));
|
|
5294
|
+
renderTable(["Company", "Date", "Subject", "Status"], missing.map((m) => ({
|
|
5295
|
+
Company: m.company_id,
|
|
5296
|
+
Date: m.comm_date,
|
|
5297
|
+
Subject: m.subject || "",
|
|
5298
|
+
Status: m.status
|
|
5299
|
+
})));
|
|
5300
|
+
console.log();
|
|
5301
|
+
});
|
|
5302
|
+
vendor.command("pending-followups").description("Show vendor follow-ups due today or overdue").action(() => {
|
|
5303
|
+
const db = getDatabase();
|
|
5304
|
+
const pending = listPendingFollowUps(db);
|
|
5305
|
+
if (!pending.length) {
|
|
5306
|
+
console.log(chalk.green(`
|
|
5307
|
+
No pending follow-ups.
|
|
5308
|
+
`));
|
|
5309
|
+
return;
|
|
5310
|
+
}
|
|
5311
|
+
console.log(chalk.yellow(`
|
|
5312
|
+
${pending.length} pending follow-up(s):
|
|
5313
|
+
`));
|
|
5314
|
+
renderTable(["Company", "Type", "Follow-up", "Subject"], pending.map((p) => ({
|
|
5315
|
+
Company: p.company_id,
|
|
5316
|
+
Type: p.type,
|
|
5317
|
+
"Follow-up": p.follow_up_date || "",
|
|
5318
|
+
Subject: p.subject || ""
|
|
5319
|
+
})));
|
|
5320
|
+
console.log();
|
|
5321
|
+
});
|
|
5322
|
+
vendor.command("history <company-id>").description("Show vendor communication history for a company").action((id) => {
|
|
5323
|
+
const db = getDatabase();
|
|
5324
|
+
const company = getCompany(id);
|
|
5325
|
+
if (!company) {
|
|
5326
|
+
console.error(chalk.red(`
|
|
5327
|
+
Company not found: ${id}
|
|
5328
|
+
`));
|
|
5329
|
+
process.exit(1);
|
|
5330
|
+
}
|
|
5331
|
+
const comms = listVendorCommunications(id, {}, db);
|
|
5332
|
+
if (!comms.length) {
|
|
5333
|
+
console.log(chalk.gray(`
|
|
5334
|
+
No communications logged for ${company.name}.
|
|
5335
|
+
`));
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
console.log(chalk.bold(`
|
|
5339
|
+
Communications: ${company.name} (${comms.length})
|
|
5340
|
+
`));
|
|
5341
|
+
renderTable(["Date", "Type", "Direction", "Subject", "Status"], comms.map((c) => ({
|
|
5342
|
+
Date: c.comm_date,
|
|
5343
|
+
Type: c.type,
|
|
5344
|
+
Direction: c.direction,
|
|
5345
|
+
Subject: c.subject || "",
|
|
5346
|
+
Status: c.status
|
|
5347
|
+
})));
|
|
5348
|
+
console.log();
|
|
5349
|
+
});
|
|
5350
|
+
var taskCmd = program.command("task").description("Contact-assigned tasks with escalation");
|
|
5351
|
+
taskCmd.command("create").description("Create a contact task").option("--contact <id>", "Contact ID (required)").option("--title <title>", "Task title (required)").option("--deadline <date>", "Deadline (YYYY-MM-DD)").option("--priority <p>", "Priority: low|medium|high|critical", "medium").option("--entity <id>", "Entity/company this task is for").option("--escalate <rule>", "Escalation rule: contactId:afterDays (repeatable)", collect, []).action(async (opts) => {
|
|
5352
|
+
const db = getDatabase();
|
|
5353
|
+
let contactId = opts.contact;
|
|
5354
|
+
let title = opts.title;
|
|
5355
|
+
if (!contactId) {
|
|
5356
|
+
contactId = await prompt("Contact ID (required):");
|
|
5357
|
+
if (!contactId) {
|
|
5358
|
+
console.error(chalk.red("Contact ID is required."));
|
|
5359
|
+
process.exit(1);
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
if (!title) {
|
|
5363
|
+
title = await prompt("Task title (required):");
|
|
5364
|
+
if (!title) {
|
|
5365
|
+
console.error(chalk.red("Task title is required."));
|
|
5366
|
+
process.exit(1);
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
const escalationRules = opts.escalate.map((rule) => {
|
|
5370
|
+
const [contactIdPart, daysPart] = rule.split(":");
|
|
5371
|
+
return {
|
|
5372
|
+
escalate_to_contact_id: contactIdPart || "",
|
|
5373
|
+
after_days: parseInt(daysPart || "7", 10),
|
|
5374
|
+
method: "email"
|
|
5375
|
+
};
|
|
5376
|
+
});
|
|
5377
|
+
const contact = getContact(contactId);
|
|
5378
|
+
const task = createContactTask({
|
|
5379
|
+
title,
|
|
5380
|
+
contact_id: contactId,
|
|
5381
|
+
deadline: opts.deadline,
|
|
5382
|
+
priority: opts.priority || "medium",
|
|
5383
|
+
entity_id: opts.entity,
|
|
5384
|
+
escalation_rules: escalationRules.length ? escalationRules : undefined
|
|
5385
|
+
}, db);
|
|
5386
|
+
console.log(chalk.green(`
|
|
5387
|
+
\u2713 Task created for ${contact.display_name}: ${task.title} (${task.id})
|
|
5388
|
+
`));
|
|
5389
|
+
});
|
|
5390
|
+
taskCmd.command("list [contact-id]").description("List contact tasks").option("--overdue", "Show only overdue tasks").option("--status <s>", "Filter by status").action((contactId, opts) => {
|
|
5391
|
+
const db = getDatabase();
|
|
5392
|
+
let tasks;
|
|
5393
|
+
if (opts.overdue) {
|
|
5394
|
+
tasks = listOverdueTasks(db);
|
|
5395
|
+
if (contactId)
|
|
5396
|
+
tasks = tasks.filter((t) => t.contact_id === contactId);
|
|
5397
|
+
} else {
|
|
5398
|
+
tasks = listContactTasks({
|
|
5399
|
+
contact_id: contactId,
|
|
5400
|
+
status: opts.status
|
|
5401
|
+
}, db);
|
|
5402
|
+
}
|
|
5403
|
+
if (!tasks.length) {
|
|
5404
|
+
console.log(chalk.gray(`
|
|
5405
|
+
No tasks found.
|
|
5406
|
+
`));
|
|
5407
|
+
return;
|
|
5408
|
+
}
|
|
5409
|
+
console.log();
|
|
5410
|
+
renderTable(["Title", "Contact", "Priority", "Status", "Deadline"], tasks.map((t) => {
|
|
5411
|
+
let contactName = t.contact_id;
|
|
5412
|
+
try {
|
|
5413
|
+
contactName = getContact(t.contact_id).display_name;
|
|
5414
|
+
} catch {}
|
|
5415
|
+
return {
|
|
5416
|
+
Title: t.title,
|
|
5417
|
+
Contact: contactName,
|
|
5418
|
+
Priority: t.priority,
|
|
5419
|
+
Status: t.status,
|
|
5420
|
+
Deadline: t.deadline ? t.deadline.slice(0, 10) : ""
|
|
5421
|
+
};
|
|
5422
|
+
}));
|
|
5423
|
+
console.log(chalk.gray(`
|
|
5424
|
+
${tasks.length} task(s)
|
|
5425
|
+
`));
|
|
5426
|
+
});
|
|
5427
|
+
taskCmd.command("done <id>").description("Mark a contact task as completed").action((id) => {
|
|
5428
|
+
const db = getDatabase();
|
|
5429
|
+
const updated = updateContactTask(id, { status: "completed" }, db);
|
|
5430
|
+
console.log(chalk.green(`
|
|
5431
|
+
\u2713 Task completed: ${updated.title}
|
|
5432
|
+
`));
|
|
5433
|
+
});
|
|
5434
|
+
var appsCmd = program.command("applications").description("Track grant/credit/program applications");
|
|
5435
|
+
appsCmd.command("list").description("List applications").option("--status <s>", "Filter by status").option("--type <t>", "Filter by type").action((opts) => {
|
|
5436
|
+
const db = getDatabase();
|
|
5437
|
+
const apps = listApplications({
|
|
5438
|
+
status: opts.status,
|
|
5439
|
+
type: opts.type
|
|
5440
|
+
}, db);
|
|
5441
|
+
if (!apps.length) {
|
|
5442
|
+
console.log(chalk.gray(`
|
|
5443
|
+
No applications found.
|
|
5444
|
+
`));
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
console.log();
|
|
5448
|
+
renderTable(["Program", "Type", "Status", "Value", "Follow-up"], apps.map((a) => ({
|
|
5449
|
+
Program: a.program_name,
|
|
5450
|
+
Type: a.type,
|
|
5451
|
+
Status: a.status,
|
|
5452
|
+
Value: a.value_usd ? `$${a.value_usd.toLocaleString()}` : "",
|
|
5453
|
+
"Follow-up": a.follow_up_date ? a.follow_up_date.slice(0, 10) : ""
|
|
5454
|
+
})));
|
|
5455
|
+
console.log(chalk.gray(`
|
|
5456
|
+
${apps.length} application(s)
|
|
5457
|
+
`));
|
|
5458
|
+
});
|
|
5459
|
+
appsCmd.command("add").description("Add a new application").option("--name <name>", "Program name (required)").option("--type <type>", "Type: ai_credits|grant|startup_program|other", "other").option("--value <usd>", "Value in USD").option("--status <status>", "Status: draft|submitted|pending|approved|rejected|follow_up_needed|expired|cancelled", "draft").option("--follow-up <date>", "Follow-up date (YYYY-MM-DD)").option("--provider <id>", "Provider company ID").option("--contact <id>", "Primary contact ID").option("--method <method>", "Application method: email|form|typeform|hubspot|manual|browser|feathery|other").option("--url <url>", "Form URL").action(async (opts) => {
|
|
5460
|
+
const db = getDatabase();
|
|
5461
|
+
let name = opts.name;
|
|
5462
|
+
if (!name) {
|
|
5463
|
+
name = await prompt("Program name (required):");
|
|
5464
|
+
if (!name) {
|
|
5465
|
+
console.error(chalk.red("Program name is required."));
|
|
5466
|
+
process.exit(1);
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
const app = createApplication({
|
|
5470
|
+
program_name: name,
|
|
5471
|
+
type: opts.type || "other",
|
|
5472
|
+
value_usd: opts.value ? parseFloat(opts.value) : undefined,
|
|
5473
|
+
status: opts.status || "draft",
|
|
5474
|
+
follow_up_date: opts.followUp,
|
|
5475
|
+
provider_company_id: opts.provider,
|
|
5476
|
+
primary_contact_id: opts.contact,
|
|
5477
|
+
method: opts.method,
|
|
5478
|
+
form_url: opts.url
|
|
5479
|
+
}, db);
|
|
5480
|
+
console.log(chalk.green(`
|
|
5481
|
+
\u2713 Application created: ${app.program_name} (${app.id})
|
|
5482
|
+
`));
|
|
5483
|
+
});
|
|
5484
|
+
appsCmd.command("followup").description("Show applications with follow-up due").action(() => {
|
|
5485
|
+
const db = getDatabase();
|
|
5486
|
+
const apps = listFollowUpDue(db);
|
|
5487
|
+
if (!apps.length) {
|
|
5488
|
+
console.log(chalk.green(`
|
|
5489
|
+
No follow-ups due.
|
|
5490
|
+
`));
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
console.log(chalk.yellow(`
|
|
5494
|
+
${apps.length} application(s) need follow-up:
|
|
5495
|
+
`));
|
|
5496
|
+
renderTable(["Program", "Type", "Status", "Follow-up"], apps.map((a) => ({
|
|
5497
|
+
Program: a.program_name,
|
|
5498
|
+
Type: a.type,
|
|
5499
|
+
Status: a.status,
|
|
5500
|
+
"Follow-up": a.follow_up_date ? a.follow_up_date.slice(0, 10) : ""
|
|
5501
|
+
})));
|
|
5502
|
+
console.log();
|
|
5503
|
+
});
|
|
5504
|
+
program.command("seed").description("Seed demo contacts, companies, and relationships").option("--demo", "Seed demo data (professional services example)").option("--clear", "Clear existing demo data first").action(async (opts) => {
|
|
5505
|
+
if (!opts.demo) {
|
|
5506
|
+
console.log("Use --demo flag to seed demo data");
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
const db = getDatabase();
|
|
5510
|
+
console.log(chalk.cyan("Seeding demo contacts..."));
|
|
5511
|
+
const kpmg = createCompany({ name: "KPMG Romania", industry: "Accounting", description: "Romanian accounting, tax, and payroll firm" }, db);
|
|
5512
|
+
const escalon = createCompany({ name: "Escalon Services", industry: "Tax & Finance", description: "US tax preparation for multi-entity businesses", domain: "escalon.services" }, db);
|
|
5513
|
+
const revisionLegal = createCompany({ name: "Revision Legal PLLC", industry: "Legal", description: "Trademark and IP law firm", domain: "revisionlegal.com" }, db);
|
|
5514
|
+
createCompany({ name: "RAW Financial", industry: "Tax & Finance", description: "US tax preparation for dissolution entities" }, db);
|
|
5515
|
+
const svb = createCompany({ name: "Silicon Valley Bank", industry: "Banking", domain: "svb.com" }, db);
|
|
5516
|
+
const hasnaInc = createCompany({ name: "Hasna, Inc.", is_owned_entity: true, entity_type: "operating", industry: "AI/Software", description: "Primary US operating entity (C-Corp, Delaware)" }, db);
|
|
5517
|
+
const beepMedia = createCompany({ name: "Beep Media International LLC", is_owned_entity: true, entity_type: "operating", industry: "Media/Technology" }, db);
|
|
5518
|
+
createCompany({ name: "Hasna Global SRL", is_owned_entity: true, entity_type: "operating", industry: "AI/Software", description: "Romanian operating entity" }, db);
|
|
5519
|
+
const alina = createContact({ display_name: "Alina Turlea", first_name: "Alina", last_name: "Turlea", job_title: "Tax Consultant", emails: [{ address: "alinaturlea@kpmg.com", type: "work", is_primary: true }], source: "manual" });
|
|
5520
|
+
const lucia = createContact({ display_name: "Lucia Grecu", first_name: "Lucia", last_name: "Grecu", job_title: "Compliance & Billing", emails: [{ address: "lsipos@kpmg.com", type: "work", is_primary: true }], source: "manual" });
|
|
5521
|
+
const elizabeth = createContact({ display_name: "Elizabeth Robles", first_name: "Elizabeth", last_name: "Robles", job_title: "Tax Manager", emails: [{ address: "elizabeth.robles@escalon.services", type: "work", is_primary: true }], source: "manual" });
|
|
5522
|
+
const drew = createContact({ display_name: "Andrew Jurgensen", first_name: "Andrew", last_name: "Jurgensen", job_title: "Partner", emails: [{ address: "drew@revisionlegal.com", type: "work", is_primary: true }], source: "manual" });
|
|
5523
|
+
const donna = createContact({ display_name: "Donna Yang", first_name: "Donna", last_name: "Yang", job_title: "Relationship Manager", emails: [{ address: "DYang@svb.com", type: "work", is_primary: true }], source: "manual" });
|
|
5524
|
+
createCompanyRelationship({ contact_id: alina.id, company_id: kpmg.id, relationship_type: "accountant", is_primary: true }, db);
|
|
5525
|
+
createCompanyRelationship({ contact_id: lucia.id, company_id: kpmg.id, relationship_type: "accountant" }, db);
|
|
5526
|
+
createCompanyRelationship({ contact_id: elizabeth.id, company_id: escalon.id, relationship_type: "tax_preparer", is_primary: true }, db);
|
|
5527
|
+
createCompanyRelationship({ contact_id: drew.id, company_id: revisionLegal.id, relationship_type: "attorney", is_primary: true }, db);
|
|
5528
|
+
createCompanyRelationship({ contact_id: donna.id, company_id: svb.id, relationship_type: "bank_manager", is_primary: true }, db);
|
|
5529
|
+
createCompanyRelationship({ contact_id: alina.id, company_id: hasnaInc.id, relationship_type: "tax_preparer", is_primary: true }, db);
|
|
5530
|
+
createCompanyRelationship({ contact_id: alina.id, company_id: beepMedia.id, relationship_type: "tax_preparer", is_primary: true }, db);
|
|
5531
|
+
console.log(chalk.green(`\u2713 Seeded: 5 companies, 3 owned entities, 5 key contacts`));
|
|
5532
|
+
console.log(chalk.gray(" Try: contacts entities list"));
|
|
5533
|
+
console.log(chalk.gray(" Try: contacts workload " + alina.id));
|
|
5534
|
+
});
|
|
4658
5535
|
program.parse(process.argv);
|