← back to Rc Fm Callnotes
lib/fm.js
91 lines
// Thin wrapper over the filemaker-mcp Data API client (whitelisted files only).
// Loads that project's .env so Claris credentials stay in one place.
import { readFileSync } from 'node:fs';
const FM_DIR = '/Users/macstudio3/Projects/filemaker-mcp';
for (const line of readFileSync(`${FM_DIR}/.env`, 'utf8').split('\n')) {
const m = line.match(/^([A-Z_]+)=(.*)$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
const fm = await import(`${FM_DIR}/src/fm-client.js`);
export const { findRecords, createRecord, updateRecord, listRecords, getRecord } = fm;
export const CLIENTS_LAYOUT = 'Main Menu Copy'; // exposes Phone + Notes (writable)
export const INVOICE_LAYOUT = 'PACKING LIST !!'; // exposes Invoice + Internal notes
// Digits-only canonical form, US numbers reduced to 10 digits.
export function normPhone(raw) {
let d = String(raw || '').replace(/\D/g, '');
if (d.length === 11 && d.startsWith('1')) d = d.slice(1);
return d;
}
// Common ways a 10-digit US number appears in the Clients file.
export function phoneVariants(digits10) {
if (digits10.length !== 10) return [`*${digits10}*`];
const a = digits10.slice(0, 3), b = digits10.slice(3, 6), c = digits10.slice(6);
return [
`*${a}-${b}-${c}*`,
`*(${a}) ${b}-${c}*`,
`*(${a})${b}-${c}*`,
`*${a}.${b}.${c}*`,
`*${a} ${b} ${c}*`,
`*${a}${b}${c}*`,
];
}
// Find a client by phone number; returns first match or null.
export async function findClientByPhone(rawNumber) {
const digits = normPhone(rawNumber);
if (digits.length < 7) return null;
const query = phoneVariants(digits).map((v) => ({ Phone: v }));
try {
const res = await findRecords('Clients', CLIENTS_LAYOUT, query, { limit: 3 });
return res.records?.[0] || null;
} catch (e) {
if (e.fmCode === '401') return null; // 401 = no records match
throw e;
}
}
// Append text to a client's Notes (read-modify-write; newest entry on top).
export async function appendClientNote(recordId, existingNotes, entry) {
const merged = entry + (existingNotes ? `\n\n${existingNotes}` : '');
return updateRecord('Clients', CLIENTS_LAYOUT, recordId, { Notes: merged }, { dryRun: false });
}
export async function createClientFromCall({ name, phone, note }) {
const fieldData = {
Company: name || 'Unknown caller',
Name: name || '',
Phone: phone,
Notes: note,
'Date Added': new Date().toLocaleDateString('en-US'),
};
// 'Date Added' isn't on Main Menu Copy — only send fields the layout exposes.
delete fieldData['Date Added'];
return createRecord('Clients', CLIENTS_LAYOUT, fieldData, { dryRun: false });
}
export async function findInvoiceByNumber(invoiceNo) {
try {
const res = await findRecords('invoice', INVOICE_LAYOUT, { Invoice: `==${invoiceNo}` }, { limit: 2 });
return res.records?.[0] || null;
} catch (e) {
if (e.fmCode === '401') return null;
throw e;
}
}
// The PACKING LIST !! layout exposes the field as "Internal notes(1)" on reads.
export function invoiceNotesOf(record) {
const f = record?.fieldData || {};
return f['Internal notes'] ?? f['Internal notes(1)'] ?? '';
}
export async function appendInvoiceInternalNote(recordId, existingNotes, entry) {
const merged = entry + (existingNotes ? `\n${existingNotes}` : '');
return updateRecord('invoice', INVOICE_LAYOUT, recordId, { 'Internal notes': merged }, { dryRun: false });
}