@profullstack/tleehealth 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # @profullstack/tleehealth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The [tleehealth](https://tleehealth.com) CLI and front-desk TUI.
4
+
5
+ ```sh
6
+ npm i -g @profullstack/tleehealth # or: curl -fsSL https://tleehealth.com/install.sh | sh
7
+ tleehealth schedule --date today
8
+ tleehealth tui
9
+ ```
10
+
11
+ `TLEEHEALTH_API_KEY` and `TLEEHEALTH_URL` override the saved config. The schedule serves demo data until accounts launch.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main().then(
5
+ (code) => process.exit(code ?? 0),
6
+ (err) => {
7
+ process.stderr.write(`tleehealth: ${err?.message ?? err}\n`);
8
+ process.exit(1);
9
+ },
10
+ );
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@profullstack/tleehealth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "tleehealth from the terminal: the CLI, the front-desk TUI, and the API client the MCP server uses.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Profullstack, Inc.",
8
+ "homepage": "https://tleehealth.com",
9
+ "repository": { "type": "git", "url": "https://github.com/profullstack/tleehealth.com", "directory": "packages/cli" },
10
+ "engines": { "node": ">=20" },
11
+ "bin": { "tleehealth": "./bin/tleehealth.mjs" },
12
+ "exports": { ".": "./src/cli.js", "./client": "./src/client.js" },
13
+ "files": ["bin", "src", "README.md"],
14
+ "dependencies": { "@profullstack/hqtui": "^0.8.0" },
15
+ "keywords": ["telehealth", "practice-management", "scheduling", "cli", "tui"]
16
+ }
package/src/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * tleehealth: the practice from the terminal.
3
+ *
4
+ * tleehealth schedule [--date today|tomorrow|YYYY-MM-DD] [--json]
5
+ * tleehealth health
6
+ * tleehealth login [KEY]
7
+ * tleehealth tui
8
+ *
9
+ * TLEEHEALTH_API_KEY and TLEEHEALTH_URL override the saved config.
10
+ */
11
+ import { createInterface } from 'node:readline/promises';
12
+ import { health, resolveAuth, saveConfig, schedule } from './client.js';
13
+
14
+ const HELP = `tleehealth: telehealth and practice management.
15
+
16
+ tleehealth schedule [--date today|tomorrow|YYYY-MM-DD] [--json] the front-desk day
17
+ tleehealth health is the API up
18
+ tleehealth login [KEY] save an API key
19
+ tleehealth tui the day view, as a screen
20
+
21
+ Environment: TLEEHEALTH_API_KEY, TLEEHEALTH_URL.`;
22
+
23
+ export function parse(argv) {
24
+ const positional = [];
25
+ const flags = {};
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const arg = argv[i];
28
+ if (arg.startsWith('--')) {
29
+ const [name, inline] = arg.slice(2).split('=', 2);
30
+ if (inline !== undefined) flags[name] = inline;
31
+ else if (argv[i + 1] && !argv[i + 1].startsWith('--')) flags[name] = argv[++i];
32
+ else flags[name] = true;
33
+ } else if (arg === '-h') flags.help = true;
34
+ else positional.push(arg);
35
+ }
36
+ return { positional, flags };
37
+ }
38
+
39
+ export function formatDay(day) {
40
+ const lines = [`${day.date}${day.demo ? ' (demo data)' : ''}`];
41
+ for (const a of day.appointments)
42
+ lines.push(
43
+ `${a.time} ${a.location.padEnd(11)} ${a.provider.padEnd(11)} ${a.patient.padEnd(11)} ${a.type.padEnd(14)} ${a.mode.padEnd(9)} ${a.reminder_call}`,
44
+ );
45
+ return lines.join('\n');
46
+ }
47
+
48
+ export async function main(argv = process.argv.slice(2)) {
49
+ const { positional, flags } = parse(argv);
50
+ const [cmd, ...rest] = positional;
51
+ if (!cmd || flags.help || cmd === 'help') {
52
+ console.log(HELP);
53
+ return 0;
54
+ }
55
+ const auth = await resolveAuth({ server: flags.server, key: flags.key });
56
+ switch (cmd) {
57
+ case 'health': {
58
+ const h = await health(auth);
59
+ console.log(`${auth.server} ok=${h.ok} v${h.version}`);
60
+ return h.ok ? 0 : 1;
61
+ }
62
+ case 'schedule': {
63
+ const day = await schedule(auth, flags.date);
64
+ console.log(flags.json ? JSON.stringify(day, null, 2) : formatDay(day));
65
+ return 0;
66
+ }
67
+ case 'login': {
68
+ let key = rest[0];
69
+ if (!key) {
70
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
71
+ key = (await rl.question('API key: ')).trim();
72
+ rl.close();
73
+ }
74
+ const file = await saveConfig({ key, server: auth.server });
75
+ process.stderr.write(`Saved to ${file}.\n`);
76
+ return 0;
77
+ }
78
+ case 'tui': {
79
+ const { runTui } = await import('./tui.js');
80
+ await runTui(auth, flags.date);
81
+ return 0;
82
+ }
83
+ default:
84
+ throw new Error(`unknown command ${cmd}; try tleehealth help`);
85
+ }
86
+ }
package/src/client.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The tleehealth API, as function calls. Shared by the CLI, the TUI and the MCP
3
+ * server. Plain fetch, no dependencies: runs on Node 20+ or Bun.
4
+ */
5
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+
9
+ const CONFIG_FILE = join(
10
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
11
+ 'tleehealth',
12
+ 'config.json',
13
+ );
14
+
15
+ export async function loadConfig() {
16
+ try {
17
+ return JSON.parse(await readFile(CONFIG_FILE, 'utf8'));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ export async function saveConfig(patch) {
24
+ const next = { ...(await loadConfig()), ...patch };
25
+ await mkdir(dirname(CONFIG_FILE), { recursive: true });
26
+ await writeFile(CONFIG_FILE, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
27
+ return CONFIG_FILE;
28
+ }
29
+
30
+ /** Server and key: flags and environment first, then the saved config. */
31
+ export async function resolveAuth({ server, key } = {}) {
32
+ const saved = await loadConfig();
33
+ return {
34
+ server: (server || process.env.TLEEHEALTH_URL || saved.server || 'https://tleehealth.com').replace(/\/+$/, ''),
35
+ key: key || process.env.TLEEHEALTH_API_KEY || saved.key || '',
36
+ };
37
+ }
38
+
39
+ export class ApiError extends Error {
40
+ constructor(status, body) {
41
+ super(body?.error ? `${body.error} (HTTP ${status})` : `HTTP ${status}`);
42
+ this.status = status;
43
+ this.body = body;
44
+ }
45
+ }
46
+
47
+ export async function call(auth, path, init = {}) {
48
+ const res = await fetch(`${auth.server}${path}`, {
49
+ ...init,
50
+ headers: { ...(auth.key ? { authorization: `Bearer ${auth.key}` } : {}), ...(init.headers ?? {}) },
51
+ });
52
+ const type = res.headers.get('content-type') ?? '';
53
+ const body = type.includes('json') ? await res.json().catch(() => ({})) : await res.text();
54
+ if (!res.ok) throw new ApiError(res.status, typeof body === 'string' ? { error: body.slice(0, 200) } : body);
55
+ return body;
56
+ }
57
+
58
+ export const health = (auth) => call(auth, '/api/v1/health');
59
+
60
+ /** The front-desk day: appointments across every location, with reminder-call state. */
61
+ export function schedule(auth, date = today()) {
62
+ return call(auth, `/api/v1/schedule?date=${encodeURIComponent(resolveDate(date))}`);
63
+ }
64
+
65
+ export function today() {
66
+ return new Date().toISOString().slice(0, 10);
67
+ }
68
+
69
+ export function resolveDate(input) {
70
+ if (!input || input === 'today') return today();
71
+ if (input === 'tomorrow') return new Date(Date.now() + 86_400_000).toISOString().slice(0, 10);
72
+ if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
73
+ throw new Error(`date is today, tomorrow or YYYY-MM-DD, not "${input}"`);
74
+ }
package/src/tui.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * tleehealth tui: the front-desk day view.
3
+ *
4
+ * Today's appointments across every location, with the AI reminder-call state of
5
+ * each. Up/Down picks a row, Left/Right moves a day, r refreshes, Ctrl+C or q quits.
6
+ */
7
+ import { createApp } from '@profullstack/hqtui';
8
+ import { resolveDate, schedule } from './client.js';
9
+
10
+ const shift = (date, days) => new Date(Date.parse(`${date}T12:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10);
11
+
12
+ export async function runTui(auth, date) {
13
+ const state = { date: resolveDate(date), day: null, selected: 0, status: 'Loading…' };
14
+ const app = await createApp({ fps: 20, quitKeys: [] });
15
+
16
+ const load = async () => {
17
+ state.status = 'Loading…';
18
+ app.invalidate();
19
+ try {
20
+ state.day = await schedule(auth, state.date);
21
+ state.selected = 0;
22
+ state.status = `${state.day.appointments.length} appointments${state.day.demo ? ' · demo data' : ''}`;
23
+ } catch (err) {
24
+ state.day = null;
25
+ state.status = err.message;
26
+ }
27
+ app.invalidate();
28
+ };
29
+
30
+ app.on('key', (event) => {
31
+ if (event.key === 'ctrl+c' || event.char === 'q') return app.quit();
32
+ const rows = state.day?.appointments ?? [];
33
+ if (event.name === 'up' || event.name === 'down') {
34
+ if (rows.length) state.selected = (state.selected + (event.name === 'up' ? -1 : 1) + rows.length) % rows.length;
35
+ } else if (event.name === 'left' || event.name === 'right') {
36
+ state.date = shift(state.date, event.name === 'left' ? -1 : 1);
37
+ return void load();
38
+ } else if (event.char === 'r') return void load();
39
+ app.invalidate();
40
+ });
41
+
42
+ app.render(({ ui, theme }) => {
43
+ ui.column({ gap: 1 }, (root) => {
44
+ root.panel({ title: `tleehealth · front desk · ${state.date}` }, (panel) => {
45
+ panel.text('Up/Down pick · Left/Right day · r refresh · q quit', { fg: theme.muted });
46
+ panel.text(state.status, { fg: theme.muted });
47
+ });
48
+ const rows = state.day?.appointments ?? [];
49
+ const locations = [...new Set(rows.map((a) => a.location))];
50
+ for (const loc of locations) {
51
+ root.panel({ title: loc }, (panel) => {
52
+ for (const a of rows) {
53
+ if (a.location !== loc) continue;
54
+ const i = rows.indexOf(a);
55
+ const mark = i === state.selected ? '›' : ' ';
56
+ panel.text(
57
+ `${mark} ${a.time} ${a.provider.padEnd(11)} ${a.patient.padEnd(11)} ${a.type.padEnd(14)} ${a.mode.padEnd(9)} call: ${a.reminder_call}`,
58
+ { fg: i === state.selected ? theme.accent : undefined },
59
+ );
60
+ }
61
+ });
62
+ }
63
+ });
64
+ });
65
+
66
+ void load();
67
+ await app.start();
68
+ }