life-pulse 2.2.2 → 2.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/installer.js CHANGED
@@ -19,7 +19,6 @@ import { join } from 'path';
19
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
20
20
  import { execSync } from 'child_process';
21
21
  import { createInterface } from 'readline';
22
- import chalk from 'chalk';
23
22
  import { runPermissionFlow, hasRequiredPermissions } from './permissions.js';
24
23
  import { discoverPlatforms, savePlatforms } from './platforms.js';
25
24
  import { runDiscovery, formatDiscoveryReport } from './icloud-discovery.js';
@@ -29,9 +28,10 @@ import { updateAutoKnowledge } from './knowledge.js';
29
28
  import { scanForSkills } from './skill-loader.js';
30
29
  import { hasTailscale, getHostname as getTailscaleHostname, startFunnel } from './tunnel.js';
31
30
  import { startGateway } from './sms-gateway.js';
32
- import { renderCRMList, renderBrief, Spinner, GRN, DIM, HD, MID, RED } from './tui.js';
31
+ import { renderCRMList, renderBrief, Spinner, DIM, HD, MID } from './tui.js';
33
32
  import { saveContactSummaries } from './intelligence.js';
34
33
  import { startSession } from './session-progress.js';
34
+ import * as rply from './rply-client.js';
35
35
  const BASE = join(homedir(), '.life-pulse');
36
36
  const STATE_PATH = join(BASE, 'install-state.json');
37
37
  function loadState() {
@@ -61,9 +61,8 @@ function isDone(state, step) {
61
61
  }
62
62
  // ─── TUI Helpers ────────────────────────────────────────────────
63
63
  function stepHeader(num, title, done) {
64
- const icon = done ? GRN('✓') : chalk.bold.hex('#7AA2F7')(`${num}`);
65
64
  const label = done ? DIM(title) : HD(title);
66
- console.log(`\n ${icon} ${label}`);
65
+ console.log(`\n ${label}`);
67
66
  }
68
67
  async function prompt(question) {
69
68
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -74,14 +73,24 @@ async function prompt(question) {
74
73
  });
75
74
  });
76
75
  }
76
+ // ─── Platform Hints ─────────────────────────────────────────────
77
+ const PLATFORM_HINTS = {
78
+ slack: 'sign into slack.com in Safari',
79
+ x: 'sign into x.com in Safari',
80
+ email: 'sign into gmail.com in Safari',
81
+ instagram: 'sign into instagram.com in Safari',
82
+ linkedin: 'sign into linkedin.com in Safari',
83
+ whatsApp: 'sign into web.whatsapp.com in Safari',
84
+ telegram: 'sign into web.telegram.org in Safari',
85
+ };
86
+ function platformHint(platform) {
87
+ return PLATFORM_HINTS[platform] || '';
88
+ }
77
89
  // ─── Main Installer Flow ────────────────────────────────────────
78
90
  export async function runInstaller(apiKey) {
79
91
  const state = loadState();
80
92
  const spinner = process.stdin.isTTY ? new Spinner() : undefined;
81
93
  console.log();
82
- console.log(chalk.bold.hex('#7AA2F7')(' life-pulse'));
83
- console.log(DIM(' personal AI operating system'));
84
- console.log();
85
94
  // ── Step 1: Pre-flight ──
86
95
  stepHeader(1, 'Pre-flight', isDone(state, 'preflight'));
87
96
  if (!isDone(state, 'preflight')) {
@@ -107,15 +116,15 @@ export async function runInstaller(apiKey) {
107
116
  process.env.ANTHROPIC_API_KEY = key;
108
117
  apiKey = key;
109
118
  state.apiKeySet = true;
110
- console.log(GRN(' API key saved'));
119
+ console.log(DIM(' API key saved'));
111
120
  }
112
121
  else {
113
- console.log(RED(' no API key — some features will be limited'));
122
+ console.log(DIM(' no API key — some features will be limited'));
114
123
  }
115
124
  }
116
125
  else {
117
126
  state.apiKeySet = true;
118
- console.log(GRN(' API key configured'));
127
+ console.log(DIM(' API key configured'));
119
128
  }
120
129
  markDone(state, 'preflight');
121
130
  }
@@ -125,13 +134,13 @@ export async function runInstaller(apiKey) {
125
134
  if (process.stdin.isTTY) {
126
135
  const allGranted = await runPermissionFlow();
127
136
  if (allGranted) {
128
- console.log(GRN(' all permissions granted'));
137
+ console.log(DIM(' all permissions granted'));
129
138
  }
130
139
  else if (hasRequiredPermissions()) {
131
140
  console.log(DIM(' required permissions granted (some optional skipped)'));
132
141
  }
133
142
  else {
134
- console.log(RED(' some required permissions missing — re-run to retry'));
143
+ console.log(HD(' some required permissions missing — re-run to retry'));
135
144
  }
136
145
  }
137
146
  markDone(state, 'permissions');
@@ -139,9 +148,9 @@ export async function runInstaller(apiKey) {
139
148
  // ── Step 3: Discovery ──
140
149
  stepHeader(3, 'Discovery', isDone(state, 'discovery'));
141
150
  if (!isDone(state, 'discovery')) {
142
- spinner?.start('scanning platforms');
151
+ spinner?.start('learning your world');
143
152
  const platformProfile = discoverPlatforms();
144
- spinner?.update('scanning iCloud');
153
+ spinner?.update('looking deeper');
145
154
  const discoveredApps = runDiscovery();
146
155
  spinner?.stop();
147
156
  if (discoveredApps.length) {
@@ -162,23 +171,45 @@ export async function runInstaller(apiKey) {
162
171
  if (skills.active.length) {
163
172
  console.log(DIM(` ${skills.active.length} skills activated`));
164
173
  }
174
+ // RPLY platform connectivity
175
+ const rplyStatus = await rply.status();
176
+ if (rplyStatus) {
177
+ const active = rplyStatus.platforms.filter(p => p.is_available && p.is_enabled);
178
+ const inactive = rplyStatus.platforms.filter(p => !p.is_available || !p.is_enabled);
179
+ console.log();
180
+ for (const p of active) {
181
+ console.log(` ${HD(p.display_name)}`);
182
+ }
183
+ for (const p of inactive) {
184
+ const hint = platformHint(p.platform);
185
+ console.log(` ${DIM('○')} ${DIM(p.display_name)}${hint ? ' ' + DIM(hint) : ''}`);
186
+ }
187
+ if (inactive.length) {
188
+ console.log();
189
+ console.log(DIM(' sign into these to connect them'));
190
+ console.log(DIM(' run setup again after'));
191
+ }
192
+ }
193
+ else {
194
+ console.log(DIM(' iMessage only — more platforms available'));
195
+ }
165
196
  savePlatforms(platformProfile);
166
197
  markDone(state, 'discovery');
167
198
  }
168
199
  // ── Step 4: CRM Build ──
169
200
  stepHeader(4, 'Relationship Map', isDone(state, 'crm'));
170
201
  if (!isDone(state, 'crm')) {
171
- spinner?.start('scanning messages');
172
- const crm = buildCRM();
202
+ spinner?.start('reading the room');
203
+ const crm = await buildCRM();
173
204
  spinner?.stop();
174
205
  if (crm.threads.length) {
175
206
  renderCRMList(crm.threads);
176
207
  // Knowledge base generation
177
- const knowledgeCount = updateAutoKnowledge(crm);
208
+ const knowledgeCount = await updateAutoKnowledge(crm);
178
209
  console.log(DIM(` ${knowledgeCount} contact knowledge bases created`));
179
210
  // Enrichment
180
211
  if (apiKey) {
181
- spinner?.start('reading the room');
212
+ spinner?.start('understanding the room');
182
213
  let brief = '';
183
214
  const enriched = [];
184
215
  for await (const t of streamEnrichedCRM(crm, apiKey, {
@@ -200,7 +231,7 @@ export async function runInstaller(apiKey) {
200
231
  // ── Step 5: Archetype ──
201
232
  stepHeader(5, 'Archetype', isDone(state, 'archetype'));
202
233
  if (!isDone(state, 'archetype') && apiKey) {
203
- spinner?.start('generating archetype');
234
+ spinner?.start('figuring out who you are');
204
235
  try {
205
236
  const platforms = discoverPlatforms(); // re-read (may have been saved)
206
237
  const archetype = await generateArchetype(platforms, apiKey);
@@ -213,48 +244,40 @@ export async function runInstaller(apiKey) {
213
244
  }
214
245
  catch (err) {
215
246
  spinner?.stop();
216
- console.log(RED(` archetype generation failed: ${err instanceof Error ? err.message : String(err)}`));
247
+ console.log(DIM(` archetype generation failed: ${err instanceof Error ? err.message : String(err)}`));
217
248
  }
218
249
  markDone(state, 'archetype');
219
250
  }
220
- // ── Step 6: Messaging Gateway (Tailscale → rply-mac-server) ──
221
- stepHeader(6, 'Messaging Gateway', isDone(state, 'phone'));
251
+ // ── Step 6: Messaging ──
252
+ stepHeader(6, 'Messaging', isDone(state, 'phone'));
222
253
  if (!isDone(state, 'phone')) {
223
- // Start the inbound gateway (rply-mac-server pushes messages here)
224
254
  if (apiKey) {
225
255
  const gw = startGateway(apiKey);
226
- console.log(GRN(` gateway listening on :${gw.port}`));
227
- // Stop after installer finishes (daemon will restart it)
228
256
  setTimeout(() => gw.stop(), 60_000);
229
257
  }
230
- // Tailscale — required for rply-mac-server to reach this Mac
231
258
  if (hasTailscale()) {
232
259
  const hostname = getTailscaleHostname();
233
260
  if (hostname) {
234
261
  state.funnelUrl = `http://${hostname}:19877`;
235
- console.log(GRN(` Tailscale: ${hostname}`));
236
- console.log(DIM(` rply-mac-server → ${state.funnelUrl}/inbound`));
237
- // Optionally expose via Funnel for HTTPS
238
- spinner?.start('setting up tunnel');
262
+ spinner?.start('opening the connection');
239
263
  const funnelUrl = startFunnel(19877);
240
264
  spinner?.stop();
241
- if (funnelUrl) {
265
+ if (funnelUrl)
242
266
  state.funnelUrl = funnelUrl;
243
- console.log(GRN(` Funnel: ${funnelUrl}/inbound`));
244
- }
267
+ console.log(DIM(' connected'));
245
268
  }
246
269
  else {
247
- console.log(RED(' Tailscale installed but no hostname check tailscale status'));
270
+ console.log(HD(' network connection incompleterun setup again'));
248
271
  }
249
272
  }
250
273
  else {
251
- console.log(RED(' Tailscale not installedrply-mac-server cannot reach this Mac'));
252
- console.log(DIM(' install: https://tailscale.com/download/mac'));
274
+ console.log(HD(' missing network layermessages won\'t reach this machine'));
275
+ console.log(DIM(' tailscale.com/download'));
253
276
  }
254
277
  markDone(state, 'phone');
255
278
  }
256
279
  // ── Step 7: Daemon Install ──
257
- stepHeader(7, 'Daemon', isDone(state, 'daemon'));
280
+ stepHeader(7, 'Background', isDone(state, 'daemon'));
258
281
  if (!isDone(state, 'daemon')) {
259
282
  const logDir = join(homedir(), 'Library/Logs/life-pulse');
260
283
  const agentsDir = join(homedir(), 'Library/LaunchAgents');
@@ -320,35 +343,31 @@ export async function runInstaller(apiKey) {
320
343
  execSync('launchctl load ~/Library/LaunchAgents/com.life-pulse.daemon.plist', {
321
344
  stdio: 'pipe', timeout: 5000,
322
345
  });
323
- console.log(GRN(' daemon installed and started'));
324
- }
325
- catch {
326
- console.log(DIM(' plists written — load manually:'));
327
- console.log(DIM(' launchctl load ~/Library/LaunchAgents/com.life-pulse.daemon.plist'));
328
346
  }
347
+ catch { }
329
348
  // Load morning
330
349
  try {
331
350
  execSync('launchctl load ~/Library/LaunchAgents/com.life-pulse.morning.plist', {
332
351
  stdio: 'pipe', timeout: 5000,
333
352
  });
334
- console.log(GRN(' morning brief: 7:30 AM daily'));
335
- }
336
- catch {
337
- console.log(DIM(' launchctl load ~/Library/LaunchAgents/com.life-pulse.morning.plist'));
338
353
  }
354
+ catch { }
355
+ console.log(DIM(' always on'));
356
+ console.log(DIM(' morning brief at 7:30 AM'));
339
357
  markDone(state, 'daemon');
340
358
  }
341
359
  // ── Step 8: First Briefing ──
342
360
  stepHeader(8, 'First Briefing', isDone(state, 'briefing'));
343
361
  if (!isDone(state, 'briefing') && apiKey && process.stdin.isTTY) {
344
362
  console.log();
345
- console.log(DIM(' running your first briefing live...'));
363
+ console.log();
364
+ console.log(DIM(' let\'s see what\'s going on...'));
346
365
  console.log();
347
366
  // Start a session for the briefing
348
367
  startSession();
349
368
  // Import and run the agent directly
350
369
  const { runAgent } = await import('./agent.js');
351
- const { pickCard, renderSectionHeader, renderSection, renderHandled, RED: tRED, AMB: tAMB, CYN: tCYN, MAG: tMAG } = await import('./tui.js');
370
+ const { pickCard, renderSectionHeader, renderSection, renderHandled, HD: tHD, DIM: tDIM, MID: tMID } = await import('./tui.js');
352
371
  const { ProgressRenderer } = await import('./progress.js');
353
372
  const renderer = new ProgressRenderer();
354
373
  renderer.start();
@@ -367,23 +386,26 @@ export async function runInstaller(apiKey) {
367
386
  if (analysis.handled?.length)
368
387
  renderHandled(analysis.handled, 5);
369
388
  if (analysis.promises?.length) {
370
- renderSectionHeader('PROMISES', tRED);
389
+ renderSectionHeader('promises', tHD);
371
390
  }
372
391
  if (analysis.alpha?.length) {
373
- renderSection('ALPHA', analysis.alpha, '', tMAG.bold, tMAG);
392
+ renderSection('alpha', analysis.alpha, '·', tHD, tMID);
374
393
  }
375
394
  markDone(state, 'briefing');
376
395
  }
377
396
  // ── Summary ──
378
397
  console.log();
379
- console.log(chalk.bold.hex('#7AA2F7')(' Setup complete'));
398
+ console.log(HD(' You\'re all set.'));
380
399
  console.log();
381
- if (state.funnelUrl) {
382
- console.log(HD(` Gateway: ${state.funnelUrl}/inbound`));
383
- console.log(DIM(' Configure rply-mac-server to POST here'));
400
+ // Show active platform count
401
+ const finalStatus = await rply.status();
402
+ if (finalStatus) {
403
+ const active = finalStatus.platforms.filter(p => p.is_available && p.is_enabled);
404
+ if (active.length) {
405
+ console.log(DIM(` ${active.map(p => p.display_name).join(', ')}`));
406
+ }
384
407
  }
385
- console.log(DIM(' Daemon running auto-restarts on crash'));
386
- console.log(DIM(' Morning brief: 7:30 AM daily'));
387
- console.log(DIM(' Run --check to verify, --status for stats'));
408
+ console.log(DIM(' running in the background'));
409
+ console.log(DIM(' you\'ll hear from me at 7:30 AM'));
388
410
  console.log();
389
411
  }
@@ -16,7 +16,7 @@ export declare function loadKnowledge(contactName: string): string;
16
16
  /** Get the knowledge directory for a contact (creates if needed) */
17
17
  export declare function getKnowledgeDir(contactName: string): string;
18
18
  /** Regenerate auto knowledge for all CRM contacts */
19
- export declare function updateAutoKnowledge(crm: CRM): number;
19
+ export declare function updateAutoKnowledge(crm: CRM): Promise<number>;
20
20
  interface ConversationTurn {
21
21
  role: 'user' | 'assistant';
22
22
  content: string;
package/dist/knowledge.js CHANGED
@@ -14,6 +14,7 @@ import { homedir } from 'os';
14
14
  import { join } from 'path';
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'fs';
16
16
  import { openDb, safeQuery } from './db.js';
17
+ import * as rply from './rply-client.js';
17
18
  const BASE = join(homedir(), '.life-pulse');
18
19
  const KNOWLEDGE_DIR = join(BASE, 'knowledge');
19
20
  const CONVERSATIONS_DIR = join(BASE, 'conversations');
@@ -93,8 +94,23 @@ function writeRelationship(dir, thread) {
93
94
  lines.push(`Waiting on them since ${thread.waitingSince}`);
94
95
  writeFileSync(join(dir, 'relationship.txt'), lines.join('\n'), 'utf-8');
95
96
  }
96
- /** Update recent-threads.txt from iMessage DB */
97
- function writeRecentThreads(dir, handle) {
97
+ /** Update recent-threads.txt RPLY first, SQLite fallback */
98
+ async function writeRecentThreads(dir, handle) {
99
+ // ── RPLY path ──
100
+ if (await rply.isAvailable()) {
101
+ const msgs = await rply.listMessages(handle, { limit: MAX_RECENT_MSGS });
102
+ if (msgs?.data.length) {
103
+ const lines = msgs.data
104
+ .filter(m => m.content?.type === 'text' && m.content.text)
105
+ .reverse() // oldest first
106
+ .map(m => `${m.direction === 'sent' ? '→' : '←'} ${m.content.text.slice(0, 200)}`);
107
+ if (lines.length) {
108
+ writeFileSync(join(dir, 'recent-threads.txt'), lines.join('\n'), 'utf-8');
109
+ return;
110
+ }
111
+ }
112
+ }
113
+ // ── SQLite fallback ──
98
114
  const db = openDb(join(homedir(), 'Library/Messages/chat.db'));
99
115
  if (!db)
100
116
  return;
@@ -116,8 +132,8 @@ function writeRecentThreads(dir, handle) {
116
132
  if (!rows.length)
117
133
  return;
118
134
  const lines = rows.reverse().map(r => {
119
- const dir = r.is_from_me ? '→' : '←';
120
- return `${dir} ${(r.text || '').slice(0, 200)}`;
135
+ const d = r.is_from_me ? '→' : '←';
136
+ return `${d} ${(r.text || '').slice(0, 200)}`;
121
137
  });
122
138
  writeFileSync(join(dir, 'recent-threads.txt'), lines.join('\n'), 'utf-8');
123
139
  }
@@ -126,7 +142,7 @@ function writeRecentThreads(dir, handle) {
126
142
  }
127
143
  }
128
144
  /** Regenerate auto knowledge for all CRM contacts */
129
- export function updateAutoKnowledge(crm) {
145
+ export async function updateAutoKnowledge(crm) {
130
146
  mkdirSync(KNOWLEDGE_DIR, { recursive: true });
131
147
  let count = 0;
132
148
  for (const thread of crm.threads) {
@@ -134,7 +150,7 @@ export function updateAutoKnowledge(crm) {
134
150
  continue;
135
151
  const dir = getKnowledgeDir(thread.name);
136
152
  writeRelationship(dir, thread);
137
- writeRecentThreads(dir, thread.handle);
153
+ await writeRecentThreads(dir, thread.handle);
138
154
  count++;
139
155
  }
140
156
  return count;
@@ -129,7 +129,7 @@ function fetchNewMessages() {
129
129
  }
130
130
  }
131
131
  /** Resolve a handle to a contact name */
132
- function resolveHandle(handle) {
132
+ async function resolveHandle(handle) {
133
133
  try {
134
134
  const profile = buildProfile();
135
135
  const lower = handle.toLowerCase();
@@ -141,7 +141,7 @@ function resolveHandle(handle) {
141
141
  catch { }
142
142
  // Fallback: use CRM for better resolution
143
143
  try {
144
- const crm = buildCRM();
144
+ const crm = await buildCRM();
145
145
  for (const t of crm.threads) {
146
146
  if (t.handle === handle) {
147
147
  const tier = t.msgs30d > 50 ? 'T1' : t.msgs30d > 15 ? 'T2' : t.msgs30d > 5 ? 'T3' : 'T4';
@@ -191,7 +191,7 @@ async function pollCycle(config) {
191
191
  skippedCount++;
192
192
  continue;
193
193
  }
194
- const { name, tier } = resolveHandle(msg.handle);
194
+ const { name, tier } = await resolveHandle(msg.handle);
195
195
  // Only respond to configured tiers
196
196
  if (!config.respondTiers.includes(tier)) {
197
197
  processed.add(msg.rowid);
package/dist/progress.js CHANGED
@@ -11,30 +11,30 @@ const STRUCTURE = chalk.hex('#3B3B3B');
11
11
  const OK = chalk.hex('#98C379');
12
12
  const FAIL = chalk.hex('#E06C75');
13
13
  const LABEL = {
14
- search_all_messages: 'checking messages',
15
- get_conversation: 'reading thread',
16
- profile_contact: 'looking up contact',
17
- get_messages: 'checking messages',
18
- get_unanswered_messages: 'checking unreplied texts',
19
- get_screen_time: 'checking screen time',
20
- get_browsing: 'checking browsing',
21
- get_calls: 'checking calls',
22
- scan_sources: 'scanning',
23
- lookup_contact: 'looking up contact',
24
- get_email_summary: 'checking email',
25
- get_git_activity: 'checking projects',
26
- get_recent_files: 'checking recent files',
27
- get_shell_history: 'checking history',
28
- get_notes: 'reading notes',
29
- get_claude_history: 'checking chats',
30
- get_chatgpt_history: 'checking chats',
31
- get_interests_for_plans: 'checking interests',
32
- get_calendar: 'checking calendar',
33
- get_reminders: 'checking reminders',
34
- get_notifications: 'checking notifications',
35
- discover_platforms: 'scanning apps',
36
- generate_archetype: 'building profile',
37
- search_emails: 'searching email',
14
+ search_all_messages: 'reading the room',
15
+ get_conversation: 'listening in',
16
+ profile_contact: 'learning who this is',
17
+ get_messages: 'reading the room',
18
+ get_unanswered_messages: 'finding what you missed',
19
+ get_screen_time: 'tracking your attention',
20
+ get_browsing: 'retracing your steps',
21
+ get_calls: 'checking who called',
22
+ scan_sources: 'pulling threads',
23
+ lookup_contact: 'learning who this is',
24
+ get_email_summary: 'going through your inbox',
25
+ get_git_activity: 'seeing what you shipped',
26
+ get_recent_files: 'noticing what you touched',
27
+ get_shell_history: 'retracing your steps',
28
+ get_notes: 'reading your thoughts',
29
+ get_claude_history: 'seeing what you asked',
30
+ get_chatgpt_history: 'seeing what you asked',
31
+ get_interests_for_plans: 'understanding your world',
32
+ get_calendar: 'looking at your day',
33
+ get_reminders: 'checking what you owe yourself',
34
+ get_notifications: 'catching up',
35
+ discover_platforms: 'figuring out your world',
36
+ generate_archetype: 'figuring out who you are',
37
+ search_emails: 'digging through mail',
38
38
  };
39
39
  const MAX_VISIBLE = 4;
40
40
  export class ProgressRenderer {
package/dist/router.js CHANGED
@@ -146,7 +146,7 @@ const stats = {
146
146
  lastForwarded: null,
147
147
  };
148
148
  function log(msg) {
149
- process.stderr.write(` router: ${msg}\n`);
149
+ process.stderr.write(` ${msg}\n`);
150
150
  }
151
151
  // ─── Poll Cycle ───────────────────────────────────────────────
152
152
  async function poll() {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * RPLY API client — localhost:19851
3
+ *
4
+ * Unified messaging API. When available, replaces direct SQLite access
5
+ * to iMessage/chat.db, removing the Full Disk Access requirement.
6
+ */
7
+ export interface RPLYConversation {
8
+ id: string;
9
+ title: string;
10
+ platform: string;
11
+ last_message_date: string;
12
+ unread_count: number;
13
+ is_group_chat: boolean;
14
+ is_never_reply: boolean;
15
+ is_dismissed: boolean;
16
+ priority?: string;
17
+ participants: RPLYParticipant[];
18
+ reply?: RPLYReply;
19
+ messages_load_state: string;
20
+ }
21
+ export interface RPLYParticipant {
22
+ display_name: string;
23
+ full_name: string;
24
+ identifier: string;
25
+ platform: string;
26
+ }
27
+ export interface RPLYReply {
28
+ id: string;
29
+ text: string;
30
+ original_text: string;
31
+ classification_reason: string;
32
+ reply_date: string;
33
+ was_edited: boolean;
34
+ }
35
+ export interface RPLYMessage {
36
+ direction: 'sent' | 'received';
37
+ date: string;
38
+ content: {
39
+ type: string;
40
+ text: string;
41
+ };
42
+ id?: string;
43
+ }
44
+ export interface RPLYContact {
45
+ display_name: string;
46
+ full_name: string;
47
+ identifier: string;
48
+ platform: string;
49
+ }
50
+ export interface RPLYSearchResult {
51
+ message: RPLYMessage & {
52
+ id: string;
53
+ };
54
+ conversation_id: string;
55
+ }
56
+ export interface RPLYStatus {
57
+ conversation_count: number;
58
+ version: string;
59
+ is_loading: boolean;
60
+ platforms: {
61
+ platform: string;
62
+ display_name: string;
63
+ is_available: boolean;
64
+ is_enabled: boolean;
65
+ }[];
66
+ }
67
+ interface Paginated<T> {
68
+ data: T[];
69
+ total: number;
70
+ limit: number;
71
+ offset: number;
72
+ }
73
+ export declare function isAvailable(): Promise<boolean>;
74
+ export declare function resetCache(): void;
75
+ export declare function status(): Promise<RPLYStatus | null>;
76
+ export declare function listConversations(opts?: {
77
+ limit?: number;
78
+ offset?: number;
79
+ category?: string;
80
+ platform?: string;
81
+ }): Promise<Paginated<RPLYConversation> | null>;
82
+ export declare function getConversation(id: string): Promise<(RPLYConversation & {
83
+ messages: RPLYMessage[];
84
+ }) | null>;
85
+ export declare function listMessages(conversationId: string, opts?: {
86
+ limit?: number;
87
+ offset?: number;
88
+ }): Promise<Paginated<RPLYMessage> | null>;
89
+ export declare function searchConversations(q: string): Promise<RPLYConversation[] | null>;
90
+ export declare function searchMessages(q: string, opts?: {
91
+ limit?: number;
92
+ }): Promise<RPLYSearchResult[] | null>;
93
+ export declare function listContacts(opts?: {
94
+ limit?: number;
95
+ offset?: number;
96
+ }): Promise<Paginated<RPLYContact> | null>;
97
+ export declare function searchContacts(q: string): Promise<RPLYContact[] | null>;
98
+ export {};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * RPLY API client — localhost:19851
3
+ *
4
+ * Unified messaging API. When available, replaces direct SQLite access
5
+ * to iMessage/chat.db, removing the Full Disk Access requirement.
6
+ */
7
+ const BASE = 'http://localhost:19851';
8
+ const TIMEOUT = 5_000;
9
+ // ─── Core ────────────────────────────────────────────────────
10
+ let _available = null;
11
+ async function get(path) {
12
+ try {
13
+ const r = await fetch(`${BASE}${path}`, { signal: AbortSignal.timeout(TIMEOUT) });
14
+ if (!r.ok)
15
+ return null;
16
+ return await r.json();
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export async function isAvailable() {
23
+ if (_available !== null)
24
+ return _available;
25
+ const s = await get('/v1/status');
26
+ _available = s !== null && !s.is_loading;
27
+ return _available;
28
+ }
29
+ export function resetCache() { _available = null; }
30
+ // ─── Endpoints ───────────────────────────────────────────────
31
+ export async function status() {
32
+ return get('/v1/status');
33
+ }
34
+ export async function listConversations(opts) {
35
+ const p = new URLSearchParams();
36
+ if (opts?.limit)
37
+ p.set('limit', String(opts.limit));
38
+ if (opts?.offset)
39
+ p.set('offset', String(opts.offset));
40
+ if (opts?.category)
41
+ p.set('category', opts.category);
42
+ if (opts?.platform)
43
+ p.set('platform', opts.platform);
44
+ const qs = p.toString();
45
+ return get(`/v1/conversations${qs ? '?' + qs : ''}`);
46
+ }
47
+ export async function getConversation(id) {
48
+ return get(`/v1/conversations/${encodeURIComponent(id)}`);
49
+ }
50
+ export async function listMessages(conversationId, opts) {
51
+ const p = new URLSearchParams();
52
+ if (opts?.limit)
53
+ p.set('limit', String(opts.limit));
54
+ if (opts?.offset)
55
+ p.set('offset', String(opts.offset));
56
+ const qs = p.toString();
57
+ return get(`/v1/conversations/${encodeURIComponent(conversationId)}/messages${qs ? '?' + qs : ''}`);
58
+ }
59
+ export async function searchConversations(q) {
60
+ return get(`/v1/conversations/search?q=${encodeURIComponent(q)}`);
61
+ }
62
+ export async function searchMessages(q, opts) {
63
+ const p = new URLSearchParams({ q });
64
+ if (opts?.limit)
65
+ p.set('limit', String(opts.limit));
66
+ return get(`/v1/messages/search?${p}`);
67
+ }
68
+ export async function listContacts(opts) {
69
+ const p = new URLSearchParams();
70
+ if (opts?.limit)
71
+ p.set('limit', String(opts.limit));
72
+ if (opts?.offset)
73
+ p.set('offset', String(opts.offset));
74
+ const qs = p.toString();
75
+ return get(`/v1/contacts${qs ? '?' + qs : ''}`);
76
+ }
77
+ export async function searchContacts(q) {
78
+ return get(`/v1/contacts/search?q=${encodeURIComponent(q)}`);
79
+ }
@@ -87,7 +87,7 @@ async function handleInbound(req, res, apiKey) {
87
87
  catch (err) {
88
88
  stats.errors++;
89
89
  const msg = err instanceof Error ? err.message : String(err);
90
- process.stderr.write(` gateway: error for ${contactName}: ${msg}\n`);
90
+ process.stderr.write(` couldn't reply to ${contactName}\n`);
91
91
  respond(res, 500, { error: 'conversation failed' });
92
92
  }
93
93
  }
@@ -122,7 +122,7 @@ export function startGateway(apiKey) {
122
122
  respond(res, 404, { error: 'not found' });
123
123
  });
124
124
  server.listen(GATEWAY_PORT, '0.0.0.0', () => {
125
- process.stderr.write(` gateway: listening on :${GATEWAY_PORT}\n`);
125
+ // silent startup — no infrastructure noise
126
126
  });
127
127
  return {
128
128
  port: GATEWAY_PORT,