@lobu/cli 6.1.1 → 7.0.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.
Files changed (100) hide show
  1. package/dist/commands/_lib/apply/apply-cmd.d.ts +36 -0
  2. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  3. package/dist/commands/_lib/apply/apply-cmd.js +548 -40
  4. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  5. package/dist/commands/_lib/apply/client.d.ts +179 -0
  6. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  7. package/dist/commands/_lib/apply/client.js +308 -28
  8. package/dist/commands/_lib/apply/client.js.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
  10. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.js +700 -86
  12. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  13. package/dist/commands/_lib/apply/diff.d.ts +61 -3
  14. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  15. package/dist/commands/_lib/apply/diff.js +382 -92
  16. package/dist/commands/_lib/apply/diff.js.map +1 -1
  17. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  18. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.js +16 -0
  20. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  21. package/dist/commands/_lib/apply/render.d.ts +9 -0
  22. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  23. package/dist/commands/_lib/apply/render.js +80 -3
  24. package/dist/commands/_lib/apply/render.js.map +1 -1
  25. package/dist/commands/chat.d.ts.map +1 -1
  26. package/dist/commands/chat.js +9 -2
  27. package/dist/commands/chat.js.map +1 -1
  28. package/dist/commands/dev.d.ts +8 -0
  29. package/dist/commands/dev.d.ts.map +1 -1
  30. package/dist/commands/dev.js +118 -5
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/commands/eval.d.ts.map +1 -1
  33. package/dist/commands/eval.js +16 -5
  34. package/dist/commands/eval.js.map +1 -1
  35. package/dist/commands/init.d.ts +2 -0
  36. package/dist/commands/init.d.ts.map +1 -1
  37. package/dist/commands/init.js +24 -0
  38. package/dist/commands/init.js.map +1 -1
  39. package/dist/commands/memory/_lib/schema.d.ts +28 -1
  40. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  41. package/dist/commands/memory/_lib/schema.js +120 -4
  42. package/dist/commands/memory/_lib/schema.js.map +1 -1
  43. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  44. package/dist/commands/memory/_lib/seed-cmd.js +41 -18
  45. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  46. package/dist/commands/org.d.ts +4 -0
  47. package/dist/commands/org.d.ts.map +1 -1
  48. package/dist/commands/org.js +10 -0
  49. package/dist/commands/org.js.map +1 -1
  50. package/dist/commands/token.d.ts +9 -0
  51. package/dist/commands/token.d.ts.map +1 -1
  52. package/dist/commands/token.js +54 -0
  53. package/dist/commands/token.js.map +1 -1
  54. package/dist/connectors/README.md +2 -2
  55. package/dist/connectors/apple_health.ts +138 -0
  56. package/dist/connectors/apple_screen_time.ts +82 -0
  57. package/dist/connectors/browser-scraper-utils.ts +35 -3
  58. package/dist/connectors/capterra.ts +5 -1
  59. package/dist/connectors/g2.ts +5 -1
  60. package/dist/connectors/github.ts +15 -38
  61. package/dist/connectors/glassdoor.ts +5 -1
  62. package/dist/connectors/google_calendar.ts +14 -4
  63. package/dist/connectors/google_gmail.ts +6 -3
  64. package/dist/connectors/google_play.ts +10 -3
  65. package/dist/connectors/index.ts +5 -0
  66. package/dist/connectors/linkedin.ts +32 -9
  67. package/dist/connectors/local_directory.ts +91 -0
  68. package/dist/connectors/revolut.ts +572 -0
  69. package/dist/connectors/trustpilot.ts +5 -1
  70. package/dist/connectors/website.ts +1 -1
  71. package/dist/connectors/whatsapp.ts +9 -1
  72. package/dist/connectors/whatsapp_local.ts +125 -0
  73. package/dist/connectors/x.ts +17 -7
  74. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  75. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  76. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  77. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  78. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  79. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  80. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  81. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  82. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  83. package/dist/eval/types.d.ts +2 -0
  84. package/dist/eval/types.d.ts.map +1 -1
  85. package/dist/index.d.ts +11 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +68 -114
  88. package/dist/index.js.map +1 -1
  89. package/dist/internal/gateway-url.d.ts +14 -0
  90. package/dist/internal/gateway-url.d.ts.map +1 -1
  91. package/dist/internal/gateway-url.js +19 -0
  92. package/dist/internal/gateway-url.js.map +1 -1
  93. package/dist/internal/index.d.ts +1 -1
  94. package/dist/internal/index.d.ts.map +1 -1
  95. package/dist/internal/index.js +1 -1
  96. package/dist/internal/index.js.map +1 -1
  97. package/dist/server.bundle.mjs +32494 -30475
  98. package/dist/start-local.bundle.mjs +10840 -7912
  99. package/dist/templates/TESTING.md.tmpl +9 -9
  100. package/package.json +6 -6
@@ -53,6 +53,14 @@ interface RepoRef {
53
53
  repo: string;
54
54
  }
55
55
 
56
+ interface GitHubMutationResponse {
57
+ id: number;
58
+ number: number;
59
+ html_url: string;
60
+ state: string;
61
+ draft?: boolean;
62
+ }
63
+
56
64
  interface GitHubRepositoryLike {
57
65
  id?: number;
58
66
  full_name?: string;
@@ -168,13 +176,6 @@ function toIsoOrUndefined(value: unknown): string | undefined {
168
176
  return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
169
177
  }
170
178
 
171
- function stripMarkdown(code: string): string {
172
- return code
173
- .replace(/```[a-zA-Z]*\n?/g, '')
174
- .replace(/```/g, '')
175
- .trim();
176
- }
177
-
178
179
  const REPO_PROPS = {
179
180
  repo_owner: { type: 'string', minLength: 1, description: 'Repository owner' },
180
181
  repo_name: { type: 'string', minLength: 1, description: 'Repository name' },
@@ -569,7 +570,7 @@ export default class GitHubConnector extends ConnectorRuntime {
569
570
  };
570
571
 
571
572
  async sync(ctx: SyncContext): Promise<SyncResult> {
572
- const config = this.parseConfig(ctx.config);
573
+ const config = ctx.config as GitHubConfig;
573
574
  const repo = this.resolveRepo(config, {});
574
575
  const token = this.resolveToken(ctx.credentials?.accessToken, config);
575
576
  const contentType = (ctx.feedKey ?? 'issues') as GitHubContentType;
@@ -611,7 +612,7 @@ export default class GitHubConnector extends ConnectorRuntime {
611
612
 
612
613
  async execute(ctx: ActionContext): Promise<ActionResult> {
613
614
  try {
614
- const config = this.parseConfig(ctx.config);
615
+ const config = ctx.config as GitHubConfig;
615
616
  const repo = this.resolveRepo(config, ctx.input);
616
617
  const token = this.resolveToken(ctx.credentials?.accessToken, config);
617
618
 
@@ -643,10 +644,6 @@ export default class GitHubConnector extends ConnectorRuntime {
643
644
  }
644
645
  }
645
646
 
646
- private parseConfig(raw: Record<string, unknown>): GitHubConfig {
647
- return raw as GitHubConfig;
648
- }
649
-
650
647
  private resolveRepo(config: GitHubConfig, input: Record<string, unknown>): RepoRef {
651
648
  const owner = asString(input.repo_owner) ?? config.repo_owner;
652
649
  const repo = asString(input.repo_name) ?? config.repo_name;
@@ -686,7 +683,7 @@ export default class GitHubConnector extends ConnectorRuntime {
686
683
 
687
684
  private async syncContent(params: {
688
685
  repo: RepoRef;
689
- contentType: GitHubContentType;
686
+ contentType: Exclude<GitHubContentType, 'stargazers'>;
690
687
  sinceIso: string;
691
688
  labelsFilter: string[];
692
689
  token: string | null;
@@ -705,10 +702,6 @@ export default class GitHubConnector extends ConnectorRuntime {
705
702
  return await this.syncDiscussions(repo, sinceIso, token);
706
703
  case 'discussion_comments':
707
704
  return await this.syncDiscussionComments(repo, sinceIso, token);
708
- case 'stargazers':
709
- return [];
710
- default:
711
- return [];
712
705
  }
713
706
  }
714
707
 
@@ -1327,12 +1320,7 @@ export default class GitHubConnector extends ConnectorRuntime {
1327
1320
  ? input.assignees.filter((value): value is string => typeof value === 'string')
1328
1321
  : undefined;
1329
1322
 
1330
- const issue = await this.requestJson<{
1331
- id: number;
1332
- number: number;
1333
- html_url: string;
1334
- state: string;
1335
- }>({
1323
+ const issue = await this.requestJson<GitHubMutationResponse>({
1336
1324
  method: 'POST',
1337
1325
  url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues`,
1338
1326
  token,
@@ -1394,12 +1382,7 @@ export default class GitHubConnector extends ConnectorRuntime {
1394
1382
  const issueNumber = toInt(input.issue_number, 0);
1395
1383
  if (!issueNumber) return { success: false, error: 'issue_number is required' };
1396
1384
 
1397
- const issue = await this.requestJson<{
1398
- id: number;
1399
- number: number;
1400
- html_url: string;
1401
- state: string;
1402
- }>({
1385
+ const issue = await this.requestJson<GitHubMutationResponse>({
1403
1386
  method: 'PATCH',
1404
1387
  url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues/${issueNumber}`,
1405
1388
  token,
@@ -1432,13 +1415,7 @@ export default class GitHubConnector extends ConnectorRuntime {
1432
1415
  const body = asString(input.body);
1433
1416
  const draft = typeof input.draft === 'boolean' ? input.draft : undefined;
1434
1417
 
1435
- const pr = await this.requestJson<{
1436
- id: number;
1437
- number: number;
1438
- html_url: string;
1439
- state: string;
1440
- draft?: boolean;
1441
- }>({
1418
+ const pr = await this.requestJson<GitHubMutationResponse>({
1442
1419
  method: 'POST',
1443
1420
  url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/pulls`,
1444
1421
  token,
@@ -1489,7 +1466,7 @@ export default class GitHubConnector extends ConnectorRuntime {
1489
1466
  ? mergeMethod
1490
1467
  : undefined,
1491
1468
  commit_title: commitTitle,
1492
- commit_message: commitMessage ? stripMarkdown(commitMessage) : undefined,
1469
+ commit_message: commitMessage,
1493
1470
  },
1494
1471
  });
1495
1472
 
@@ -16,6 +16,8 @@ import {
16
16
  type SyncResult,
17
17
  } from '@lobu/connector-sdk';
18
18
  import {
19
+ getBrowserCdpUrl,
20
+ getBrowserUserDataDir,
19
21
  handleCookieConsent,
20
22
  openStealthBrowser,
21
23
  validateUrlDomain,
@@ -158,7 +160,9 @@ export default class GlassdoorConnector extends ConnectorRuntime {
158
160
  : `https://www.glassdoor.com/Reviews/${company_name}-reviews-SRCH_KE0.htm`;
159
161
  validateUrlDomain(baseUrl, 'glassdoor.com');
160
162
 
161
- const session = await openStealthBrowser({ cdpUrl: 'auto' });
163
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
164
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
165
+ const session = await openStealthBrowser({ cdpUrl, userDataDir });
162
166
 
163
167
  return withBrowserErrorCapture(session, 'glassdoor-sync', async (page) => {
164
168
  // Configure viewport and user-agent to mimic a real browser
@@ -256,8 +256,11 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
256
256
  let nextSyncToken: string | undefined;
257
257
 
258
258
  while (true) {
259
+ // Always request a full page — `maxResults` is a soft cap on *stored*
260
+ // events, not a reason to shrink the request size (shrinking to 1 once the
261
+ // cap is hit would crawl a busy calendar one event per round-trip).
259
262
  const params = new URLSearchParams({
260
- maxResults: String(Math.min(250, maxResults - events.length)),
263
+ maxResults: '250',
261
264
  orderBy: 'startTime',
262
265
  singleEvents: 'true',
263
266
  timeMin: timeMin.toISOString(),
@@ -280,6 +283,7 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
280
283
 
281
284
  if (data.items) {
282
285
  for (const calEvent of data.items) {
286
+ if (events.length >= maxResults) break;
283
287
  const envelope = this.calendarEventToEnvelope(calEvent);
284
288
  if (envelope) events.push(envelope);
285
289
  }
@@ -287,7 +291,12 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
287
291
 
288
292
  nextSyncToken = data.nextSyncToken;
289
293
  pageToken = data.nextPageToken;
290
- if (!pageToken || events.length >= maxResults) break;
294
+ // Google only returns nextSyncToken on the LAST page (no nextPageToken).
295
+ // Must keep paginating until pageToken is exhausted, otherwise the sync
296
+ // token is never obtained and every subsequent sync re-runs the full
297
+ // window from scratch — so we keep paging past `maxResults`, just stop
298
+ // appending events once the cap is reached.
299
+ if (!pageToken) break;
291
300
  }
292
301
 
293
302
  return this.buildResult(events, nextSyncToken, events.length);
@@ -343,7 +352,7 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
343
352
 
344
353
  while (true) {
345
354
  const params = new URLSearchParams({
346
- maxResults: String(Math.min(250, maxResults - events.length)),
355
+ maxResults: String(Math.max(1, Math.min(250, maxResults - events.length))),
347
356
  syncToken,
348
357
  });
349
358
  if (pageToken) {
@@ -375,7 +384,8 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
375
384
 
376
385
  nextSyncToken = data.nextSyncToken;
377
386
  pageToken = data.nextPageToken;
378
- if (!pageToken || events.length >= maxResults) break;
387
+ // Paginate until exhausted so we capture the trailing nextSyncToken.
388
+ if (!pageToken) break;
379
389
  }
380
390
 
381
391
  return { events, nextSyncToken };
@@ -135,7 +135,7 @@ export default class GmailConnector extends ConnectorRuntime {
135
135
  },
136
136
  entityLinks: [
137
137
  {
138
- entityType: '$member',
138
+ entityType: 'person',
139
139
  autoCreate: true,
140
140
  titlePath: 'metadata.from_name',
141
141
  identities: [{ namespace: IDENTITY.EMAIL, eventPath: 'metadata.from_email' }],
@@ -274,8 +274,11 @@ export default class GmailConnector extends ConnectorRuntime {
274
274
  return d;
275
275
  })();
276
276
 
277
- const afterStr = `${afterDate.getFullYear()}/${String(afterDate.getMonth() + 1).padStart(2, '0')}/${String(afterDate.getDate()).padStart(2, '0')}`;
278
- const query = `after:${afterStr} label:${label}`;
277
+ // Gmail's `after:` accepts a Unix timestamp (epoch seconds) for second-level
278
+ // precision. Using `YYYY/MM/DD` (day granularity, host timezone) meant every
279
+ // sync within the same day re-fetched the whole day's threads as duplicates.
280
+ const afterEpochSeconds = Math.floor(afterDate.getTime() / 1000);
281
+ const query = `after:${afterEpochSeconds} label:${label}`;
279
282
 
280
283
  const events: EventEnvelope[] = [];
281
284
  let pageToken: string | undefined;
@@ -73,9 +73,16 @@ interface RawReview {
73
73
  */
74
74
  function parseDate(dateArray: unknown): string | null {
75
75
  if (!Array.isArray(dateArray)) return null;
76
- const milliStr = String(dateArray[1] ?? '000');
77
- const totalMs = `${dateArray[0]}${milliStr.substring(0, 3)}`;
78
- return new Date(Number(totalMs)).toJSON();
76
+ // Compute numerically: seconds*1000 + millis. The previous string-concat
77
+ // approach (`${seconds}${millis}`) only worked when millis was a 3-digit
78
+ // zero-padded string; Google sends a plain integer, so e.g. `[s, 5]` produced
79
+ // a date in 1970 and `[s, 50]` a date in year ~7340.
80
+ const seconds = Number(dateArray[0]);
81
+ const millis = Number(dateArray[1] ?? 0);
82
+ if (!Number.isFinite(seconds) || !Number.isFinite(millis)) return null;
83
+ const d = new Date(seconds * 1000 + millis);
84
+ if (Number.isNaN(d.getTime())) return null;
85
+ return d.toJSON();
79
86
  }
80
87
 
81
88
  /**
@@ -1,3 +1,6 @@
1
+ export * from './apple_health.ts';
2
+ export * from './apple_screen_time.ts';
3
+ export * from './local_directory.ts';
1
4
  export * from './browser-scraper-utils.ts';
2
5
  export * from './capterra.ts';
3
6
  export * from './g2.ts';
@@ -14,10 +17,12 @@ export * from './linkedin.ts';
14
17
  export * from './microsoft_outlook.ts';
15
18
  export * from './producthunt.ts';
16
19
  export * from './reddit.ts';
20
+ export * from './revolut.ts';
17
21
  export * from './rss.ts';
18
22
  export * from './spotify.ts';
19
23
  export * from './trustpilot.ts';
20
24
  export * from './website.ts';
21
25
  export * from './whatsapp.ts';
26
+ export * from './whatsapp_local.ts';
22
27
  export * from './x.ts';
23
28
  export * from './youtube.ts';
@@ -19,7 +19,12 @@ import {
19
19
  type SyncContext,
20
20
  type SyncResult,
21
21
  } from '@lobu/connector-sdk';
22
- import { getBrowserCookies, validateCookieNotExpired } from './browser-scraper-utils';
22
+ import {
23
+ getBrowserCdpUrl,
24
+ getBrowserCookies,
25
+ getBrowserUserDataDir,
26
+ validateCookieNotExpired,
27
+ } from './browser-scraper-utils';
23
28
 
24
29
  // ── Types ──────────────────────────────────────────────────────
25
30
 
@@ -316,23 +321,37 @@ export default class LinkedInConnector extends ConnectorRuntime {
316
321
  // Normalize URL - remove trailing slash
317
322
  const baseUrl = companyUrl.replace(/\/$/, '');
318
323
 
319
- const cookies = getBrowserCookies(ctx.checkpoint as any, ctx.sessionState as any, 'linkedin');
320
- validateCookieNotExpired(cookies, 'li_at', 'linkedin');
324
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
325
+ const cdpUrlFromSession = getBrowserCdpUrl(ctx.sessionState);
326
+ const cdpUrl = cdpUrlFromSession ?? 'auto';
327
+ // No need to require cookies when the device tells us to attach directly
328
+ // (managed --user-data-dir on disk, or an explicit CDP endpoint pointed
329
+ // at the user's running Chrome). The cookie cascade is only the fallback
330
+ // for the cloud/auto path.
331
+ const skipServerCookies = !!userDataDir || !!cdpUrlFromSession;
332
+ const cookies = skipServerCookies
333
+ ? []
334
+ : getBrowserCookies(ctx.checkpoint as any, ctx.sessionState as any, 'linkedin');
335
+ if (!skipServerCookies) {
336
+ validateCookieNotExpired(cookies, 'li_at', 'linkedin');
337
+ }
321
338
 
322
339
  const maxScrolls = (config.max_scrolls as number) ?? (feedKey === 'jobs' ? 3 : 5);
323
340
 
324
341
  if (feedKey === 'jobs') {
325
- return this.syncJobs(baseUrl, cookies, maxScrolls, checkpoint);
342
+ return this.syncJobs(baseUrl, cookies, maxScrolls, checkpoint, userDataDir, cdpUrl);
326
343
  }
327
344
 
328
- return this.syncUpdates(baseUrl, cookies, maxScrolls, checkpoint);
345
+ return this.syncUpdates(baseUrl, cookies, maxScrolls, checkpoint, userDataDir, cdpUrl);
329
346
  }
330
347
 
331
348
  private async syncUpdates(
332
349
  baseUrl: string,
333
350
  cookies: any[],
334
351
  maxScrolls: number,
335
- checkpoint: LinkedInCheckpoint
352
+ checkpoint: LinkedInCheckpoint,
353
+ userDataDir: string | undefined,
354
+ cdpUrl: string | 'auto'
336
355
  ): Promise<SyncResult> {
337
356
  const postsUrl = `${baseUrl}/posts/`;
338
357
 
@@ -350,8 +369,9 @@ export default class LinkedInConnector extends ConnectorRuntime {
350
369
  navigationTimeoutMs: 20000,
351
370
  },
352
371
  url: postsUrl,
353
- cdpUrl: 'auto',
372
+ cdpUrl,
354
373
  cookies,
374
+ userDataDir,
355
375
  parseResponse: parseCompanyUpdates,
356
376
  checkAuth: async (page) => {
357
377
  const url = page.url();
@@ -401,7 +421,9 @@ export default class LinkedInConnector extends ConnectorRuntime {
401
421
  baseUrl: string,
402
422
  cookies: any[],
403
423
  maxScrolls: number,
404
- checkpoint: LinkedInCheckpoint
424
+ checkpoint: LinkedInCheckpoint,
425
+ userDataDir: string | undefined,
426
+ cdpUrl: string | 'auto'
405
427
  ): Promise<SyncResult> {
406
428
  const jobsUrl = `${baseUrl}/jobs/`;
407
429
 
@@ -420,8 +442,9 @@ export default class LinkedInConnector extends ConnectorRuntime {
420
442
  navigationTimeoutMs: 20000,
421
443
  },
422
444
  url: jobsUrl,
423
- cdpUrl: 'auto',
445
+ cdpUrl,
424
446
  cookies,
447
+ userDataDir,
425
448
  parseResponse: parseJobListings,
426
449
  checkAuth: async (page) => {
427
450
  const url = page.url();
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Local Directory Connector (V1 runtime) — Lobu for Mac only.
3
+ *
4
+ * Syncs text files (txt/md/json/csv/html) from a local folder on the user's
5
+ * Mac via Lobu for Mac. The app advertises the `local_directory`
6
+ * capability on /api/workers/poll, reads the folder, and streams file events
7
+ * back through the standard worker protocol.
8
+ *
9
+ * The sync() / execute() stubs here throw immediately if a server-side worker
10
+ * somehow bypassed the capability gate — same pattern as apple_screen_time.ts.
11
+ */
12
+
13
+ import {
14
+ type ActionResult,
15
+ type ConnectorDefinition,
16
+ ConnectorRuntime,
17
+ type SyncContext,
18
+ type SyncResult,
19
+ } from '@lobu/connector-sdk';
20
+
21
+ const BRIDGE_ONLY_MESSAGE =
22
+ 'local.directory runs only on a worker advertising capability "local_directory" (Lobu for Mac). ' +
23
+ 'This run was claimed by a worker without that capability — check connector_definitions.required_capability and the poll-time capability filter.';
24
+
25
+ export default class LocalDirectoryConnector extends ConnectorRuntime {
26
+ readonly definition: ConnectorDefinition = {
27
+ key: 'local.directory',
28
+ name: 'Local Folder',
29
+ description:
30
+ 'Sync text files (txt/md/json/csv/html) from a folder on your Mac via Lobu for Mac.',
31
+ version: '0.1.0',
32
+ faviconDomain: 'apple.com',
33
+ requiredCapability: 'local_directory',
34
+ runtime: { platforms: ['macos'] },
35
+ authSchema: { methods: [{ type: 'none' }] },
36
+ feeds: {
37
+ files: {
38
+ key: 'files',
39
+ name: 'Files',
40
+ description: 'Text files from one local folder on the user\'s Mac. One feed per folder — folder_id is an opaque stable id minted by the Mac app (the security-scoped bookmark is held device-side; the server never sees the absolute path).',
41
+ userManaged: true,
42
+ configSchema: {
43
+ type: 'object',
44
+ required: ['folder_id', 'display_name'],
45
+ properties: {
46
+ folder_id: {
47
+ type: 'string',
48
+ minLength: 8,
49
+ maxLength: 64,
50
+ description: 'Opaque stable id (UUID) minted on the Mac. Maps to a security-scoped bookmark stored locally on the device.',
51
+ },
52
+ display_name: {
53
+ type: 'string',
54
+ minLength: 1,
55
+ maxLength: 200,
56
+ description: 'Folder name shown in the UI (e.g., "Documents"). Not used to locate the folder — the device resolves folder_id to its bookmark.',
57
+ },
58
+ },
59
+ },
60
+ eventKinds: {
61
+ file_document: {
62
+ description: 'A text file from a configured local folder.',
63
+ metadataSchema: {
64
+ type: 'object',
65
+ // No absolute filesystem path — the bridge sends the folder's
66
+ // display name and the file name, which is enough context
67
+ // without leaking the user's home directory / disk layout.
68
+ required: ['source', 'folder', 'name'],
69
+ properties: {
70
+ source: { type: 'string', const: 'local_directory' },
71
+ folder: { type: 'string', description: 'Display name of the local folder.' },
72
+ name: { type: 'string', description: 'File name.' },
73
+ ext: { type: 'string' },
74
+ size_bytes: { type: 'number' },
75
+ modified_at: { type: 'string' },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ },
82
+ };
83
+
84
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
85
+ throw new Error(BRIDGE_ONLY_MESSAGE);
86
+ }
87
+
88
+ async execute(): Promise<ActionResult> {
89
+ throw new Error(BRIDGE_ONLY_MESSAGE);
90
+ }
91
+ }