floq 1.7.0 → 1.9.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.ja.md CHANGED
@@ -274,7 +274,7 @@ floq config turso --disable
274
274
 
275
275
  ## Googleカレンダー連携
276
276
 
277
- FloqはGoogleカレンダーの予定をTUIに表示できます。2つの方法があります:
277
+ FloqはGoogleカレンダーの予定をTUIに表示できます。複数のカレンダーを登録でき(iCalとOAuthの混在も可能)、有効なカレンダーすべての予定がマージして表示されます。2つの方法があります:
278
278
 
279
279
  ### 方法1: iCal URL(シンプル、認証不要)
280
280
 
@@ -284,7 +284,9 @@ GoogleカレンダーのシークレットiCal URLを使用して、OAuth設定
284
284
  # GoogleカレンダーからiCal URLを取得:
285
285
  # 設定 > (カレンダー名) > カレンダーの統合 > "iCal形式の非公開URL"
286
286
 
287
- floq calendar add "https://calendar.google.com/calendar/ical/..." -n "マイカレンダー"
287
+ floq calendar add "https://calendar.google.com/calendar/ical/..." -n "仕事"
288
+ floq calendar add "https://calendar.google.com/calendar/ical/..." -n "プライベート"
289
+ floq calendar list
288
290
  floq calendar show
289
291
  ```
290
292
 
@@ -326,10 +328,10 @@ floq calendar login
326
328
  # → ブラウザが開いて認証画面へ
327
329
  # → 表示されたコードを入力
328
330
 
329
- # カレンダーを選択
331
+ # 登録するカレンダーを選択(複数選択可)
330
332
  floq calendar select
331
333
  # → カレンダー一覧が表示される
332
- # → 番号を入力して選択
334
+ # → 番号をカンマ区切りで入力(例: 1,3)
333
335
 
334
336
  # 設定と今日の予定を表示
335
337
  floq calendar show
@@ -345,20 +347,33 @@ floq calendar logout
345
347
 
346
348
  ```bash
347
349
  # iCalモード
348
- floq calendar add <url> [-n name] # iCal URLを追加
349
- floq calendar remove # カレンダーを削除
350
+ floq calendar add <url> [-n name] # iCalカレンダーを追加(複数登録可)
350
351
 
351
352
  # OAuthモード
352
353
  floq calendar config --client-id <id> --client-secret <secret>
353
354
  floq calendar login # Google OAuthログイン
354
355
  floq calendar logout # OAuthトークンをクリア
355
- floq calendar select # カレンダー選択(対話式)
356
+ floq calendar select # Googleカレンダーを登録(対話式・複数可)
356
357
 
357
358
  # 共通コマンド
359
+ floq calendar list # 登録済みカレンダー一覧
360
+ floq calendar remove <id|番号|名前> # カレンダーを削除
361
+ floq calendar remove --all # カレンダー設定をすべて削除
358
362
  floq calendar show # 設定と今日の予定を表示
359
363
  floq calendar sync # キャッシュを更新
360
- floq calendar enable # 表示を有効化
361
- floq calendar disable # 表示を無効化
364
+ floq calendar enable [id] # 表示を有効化(全体または個別)
365
+ floq calendar disable [id] # 表示を無効化(全体または個別)
366
+ ```
367
+
368
+ ### スケジュールコマンド
369
+
370
+ 登録したすべてのカレンダーの予定をまとめて確認できます:
371
+
372
+ ```bash
373
+ floq schedule # 今日の予定
374
+ floq schedule tomorrow # 明日の予定
375
+ floq schedule week # 今後7日間
376
+ floq schedule --days 3 # 今後N日間
362
377
  ```
363
378
 
364
379
  ## テーマ
package/README.md CHANGED
@@ -274,7 +274,7 @@ floq config turso --disable
274
274
 
275
275
  ## Google Calendar Integration
276
276
 
277
- Floq can display your Google Calendar events in the TUI. Two methods are available:
277
+ Floq can display your Google Calendar events in the TUI. Multiple calendars can be registered (iCal and OAuth sources can be mixed), and events from all enabled calendars are merged. Two methods are available:
278
278
 
279
279
  ### Option 1: iCal URL (Simple, No Authentication)
280
280
 
@@ -284,7 +284,9 @@ Use Google Calendar's secret iCal URL for read-only access without OAuth setup.
284
284
  # Get your iCal URL from Google Calendar:
285
285
  # Settings > (Your Calendar) > Integrate calendar > "Secret address in iCal format"
286
286
 
287
- floq calendar add "https://calendar.google.com/calendar/ical/..." -n "My Calendar"
287
+ floq calendar add "https://calendar.google.com/calendar/ical/..." -n "Work"
288
+ floq calendar add "https://calendar.google.com/calendar/ical/..." -n "Personal"
289
+ floq calendar list
288
290
  floq calendar show
289
291
  ```
290
292
 
@@ -326,10 +328,10 @@ floq calendar login
326
328
  # → Opens browser for authentication
327
329
  # → Enter the displayed code when prompted
328
330
 
329
- # Select a calendar
331
+ # Select calendars to register (multiple selections supported)
330
332
  floq calendar select
331
333
  # → Shows list of your calendars
332
- # → Enter the number to select
334
+ # → Enter numbers, comma-separated (e.g. 1,3)
333
335
 
334
336
  # View configuration and today's events
335
337
  floq calendar show
@@ -345,20 +347,33 @@ floq calendar logout
345
347
 
346
348
  ```bash
347
349
  # iCal mode
348
- floq calendar add <url> [-n name] # Add iCal URL
349
- floq calendar remove # Remove calendar
350
+ floq calendar add <url> [-n name] # Add an iCal calendar (repeatable)
350
351
 
351
352
  # OAuth mode
352
353
  floq calendar config --client-id <id> --client-secret <secret>
353
354
  floq calendar login # Google OAuth login
354
355
  floq calendar logout # Clear OAuth tokens
355
- floq calendar select # Select calendar (interactive)
356
+ floq calendar select # Register Google calendars (interactive, multiple)
356
357
 
357
358
  # Common commands
359
+ floq calendar list # List registered calendars
360
+ floq calendar remove <id|num|name> # Remove a calendar
361
+ floq calendar remove --all # Remove all calendar configuration
358
362
  floq calendar show # Show config and today's events
359
363
  floq calendar sync # Refresh cache
360
- floq calendar enable # Enable display
361
- floq calendar disable # Disable display
364
+ floq calendar enable [id] # Enable display (all, or one calendar)
365
+ floq calendar disable [id] # Disable display (all, or one calendar)
366
+ ```
367
+
368
+ ### Schedule Command
369
+
370
+ View merged events from all registered calendars:
371
+
372
+ ```bash
373
+ floq schedule # Today's events
374
+ floq schedule tomorrow # Tomorrow's events
375
+ floq schedule week # Next 7 days
376
+ floq schedule --days 3 # Next N days
362
377
  ```
363
378
 
364
379
  ## Themes
@@ -5,9 +5,12 @@ export interface CalendarEvent {
5
5
  end: Date;
6
6
  allDay: boolean;
7
7
  location?: string;
8
+ calendarName?: string;
9
+ sourceId?: string;
8
10
  }
9
11
  /**
10
- * Fetch calendar events from the configured source (iCal URL or OAuth)
12
+ * Fetch and merge calendar events from all enabled calendar sources.
13
+ * When a URL is passed explicitly, fetch only that iCal URL (validation use; cache is not updated).
11
14
  */
12
15
  export declare function fetchCalendarEvents(url?: string): Promise<CalendarEvent[]>;
13
16
  /**
@@ -1,5 +1,5 @@
1
1
  import ICAL from 'ical.js';
2
- import { getCalendarConfig, isCalendarEnabled, getCalendarType, getCalendarOAuthConfig } from '../config.js';
2
+ import { getCalendarConfig, isCalendarEnabled, isCalendarSourceUsable } from '../config.js';
3
3
  import { getValidAccessToken } from './oauth.js';
4
4
  import { listEvents as listGoogleEvents } from './google-api.js';
5
5
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
@@ -90,29 +90,6 @@ function normalizeUrl(url) {
90
90
  }
91
91
  return url;
92
92
  }
93
- /**
94
- * Fetch calendar events via OAuth (Google Calendar API)
95
- */
96
- async function fetchEventsViaOAuth() {
97
- const oauthConfig = getCalendarOAuthConfig();
98
- if (!oauthConfig) {
99
- return [];
100
- }
101
- const accessToken = await getValidAccessToken();
102
- if (!accessToken) {
103
- return [];
104
- }
105
- const now = new Date();
106
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
107
- const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
108
- try {
109
- return await listGoogleEvents(accessToken, oauthConfig.calendarId, startOfMonth, endOfNextMonth);
110
- }
111
- catch (error) {
112
- console.error('Failed to fetch Google Calendar events:', error);
113
- return [];
114
- }
115
- }
116
93
  /**
117
94
  * Fetch calendar events via iCal URL
118
95
  */
@@ -130,28 +107,59 @@ async function fetchEventsViaIcal(url) {
130
107
  return parseICalData(icalData);
131
108
  }
132
109
  /**
133
- * Fetch calendar events from the configured source (iCal URL or OAuth)
110
+ * Fetch events for a single calendar source
111
+ */
112
+ async function fetchEventsForSource(source, accessToken) {
113
+ let events;
114
+ if (source.type === 'oauth') {
115
+ if (!accessToken || !source.calendarId) {
116
+ return [];
117
+ }
118
+ const now = new Date();
119
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
120
+ const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
121
+ events = await listGoogleEvents(accessToken, source.calendarId, startOfMonth, endOfNextMonth);
122
+ }
123
+ else {
124
+ if (!source.url) {
125
+ return [];
126
+ }
127
+ events = await fetchEventsViaIcal(source.url);
128
+ }
129
+ return events.map(event => ({
130
+ ...event,
131
+ calendarName: source.name,
132
+ sourceId: source.id,
133
+ }));
134
+ }
135
+ /**
136
+ * Fetch and merge calendar events from all enabled calendar sources.
137
+ * When a URL is passed explicitly, fetch only that iCal URL (validation use; cache is not updated).
134
138
  */
135
139
  export async function fetchCalendarEvents(url) {
140
+ if (url) {
141
+ return fetchEventsViaIcal(url);
142
+ }
136
143
  try {
137
- let events;
138
- const calendarType = getCalendarType();
139
- if (url) {
140
- // Explicit URL provided, use iCal mode
141
- events = await fetchEventsViaIcal(url);
142
- }
143
- else if (calendarType === 'oauth') {
144
- // OAuth mode
145
- events = await fetchEventsViaOAuth();
144
+ const config = getCalendarConfig();
145
+ const sources = (config?.calendars || []).filter(s => s.enabled !== false && isCalendarSourceUsable(s, config));
146
+ if (sources.length === 0) {
147
+ return [];
146
148
  }
147
- else {
148
- // iCal mode
149
- const targetUrl = getCalendarConfig()?.url;
150
- if (!targetUrl) {
151
- return [];
149
+ // Resolve the access token once if any oauth source is present
150
+ const needsOAuth = sources.some(s => s.type === 'oauth');
151
+ const accessToken = needsOAuth ? await getValidAccessToken() : null;
152
+ const results = await Promise.allSettled(sources.map(source => fetchEventsForSource(source, accessToken)));
153
+ const events = [];
154
+ results.forEach((result, index) => {
155
+ if (result.status === 'fulfilled') {
156
+ events.push(...result.value);
152
157
  }
153
- events = await fetchEventsViaIcal(targetUrl);
154
- }
158
+ else {
159
+ console.error(`Failed to fetch calendar "${sources[index].name}":`, result.reason);
160
+ }
161
+ });
162
+ events.sort((a, b) => a.start.getTime() - b.start.getTime());
155
163
  // Update cache
156
164
  eventsCache = {
157
165
  events,
@@ -1,4 +1,4 @@
1
- import { getGoogleOAuthClient, getCalendarOAuthConfig, setCalendarOAuthConfig, } from '../config.js';
1
+ import { getGoogleOAuthClient, getCalendarOAuthTokens, setCalendarOAuthTokens, } from '../config.js';
2
2
  const GOOGLE_OAUTH_DEVICE_CODE_URL = 'https://oauth2.googleapis.com/device/code';
3
3
  const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
4
4
  const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly';
@@ -125,19 +125,15 @@ export function isTokenExpired(tokens) {
125
125
  * Get a valid access token, refreshing if necessary
126
126
  */
127
127
  export async function getValidAccessToken() {
128
- const oauthConfig = getCalendarOAuthConfig();
129
- if (!oauthConfig) {
128
+ let tokens = getCalendarOAuthTokens();
129
+ if (!tokens) {
130
130
  return null;
131
131
  }
132
- let tokens = oauthConfig.tokens;
133
132
  if (isTokenExpired(tokens)) {
134
133
  try {
135
134
  tokens = await refreshAccessToken(tokens.refreshToken);
136
135
  // Update stored tokens
137
- setCalendarOAuthConfig({
138
- ...oauthConfig,
139
- tokens,
140
- });
136
+ setCalendarOAuthTokens(tokens);
141
137
  }
142
138
  catch (error) {
143
139
  console.error('Failed to refresh access token:', error);
@@ -150,5 +146,5 @@ export async function getValidAccessToken() {
150
146
  * Clear OAuth tokens (logout)
151
147
  */
152
148
  export function clearOAuthTokens() {
153
- setCalendarOAuthConfig(undefined);
149
+ setCalendarOAuthTokens(undefined);
154
150
  }
package/dist/cli.js CHANGED
@@ -6,13 +6,14 @@ import { addTask } from './commands/add.js';
6
6
  import { listTasks, listProjects } from './commands/list.js';
7
7
  import { moveTask } from './commands/move.js';
8
8
  import { markDone } from './commands/done.js';
9
- import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
9
+ import { addProject, listProjectsCommand, showProject, completeProject, deleteProjectCommand, } from './commands/project.js';
10
10
  import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, showTursoQr, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
11
11
  import { addComment, listComments } from './commands/comment.js';
12
12
  import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
13
13
  import { showInsights } from './commands/insights.js';
14
14
  import { runSetupWizard } from './commands/setup.js';
15
- import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar } from './commands/calendar.js';
15
+ import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar, listCalendarSourcesCommand } from './commands/calendar.js';
16
+ import { showSchedule } from './commands/schedule.js';
16
17
  import { VERSION } from './version.js';
17
18
  const program = new Command();
18
19
  program
@@ -96,6 +97,16 @@ projectCmd
96
97
  .action(async (id) => {
97
98
  await completeProject(id);
98
99
  });
100
+ projectCmd
101
+ .command('delete <id>')
102
+ .alias('rm')
103
+ .description('Delete a project (prompts how to handle its tasks)')
104
+ .option('--with-tasks', 'Delete the project together with all its tasks')
105
+ .option('--keep-tasks', 'Delete the project but move its tasks to Inbox')
106
+ .option('-f, --force', 'Skip confirmation prompts')
107
+ .action(async (id, options) => {
108
+ await deleteProjectCommand(id, options);
109
+ });
99
110
  // Config commands
100
111
  const configCmd = program
101
112
  .command('config')
@@ -306,22 +317,39 @@ program
306
317
  .action(async () => {
307
318
  await runSetupWizard();
308
319
  });
320
+ // Schedule command
321
+ program
322
+ .command('schedule [period]')
323
+ .description('Show schedule from registered calendars (today, tomorrow, week)')
324
+ .option('-d, --days <n>', 'Number of days to show')
325
+ .action(async (period, options) => {
326
+ await showSchedule(period, options);
327
+ });
309
328
  // Calendar commands
310
329
  const calendarCmd = program
311
330
  .command('calendar')
312
331
  .description('Google Calendar (iCal) integration');
313
332
  calendarCmd
314
333
  .command('add <url>')
315
- .description('Add or update calendar URL')
334
+ .description('Add a calendar by iCal URL (multiple calendars supported)')
316
335
  .option('-n, --name <name>', 'Display name for the calendar')
317
336
  .action(async (url, options) => {
318
337
  await addCalendar(url, options);
319
338
  });
320
339
  calendarCmd
321
- .command('remove')
322
- .description('Remove calendar configuration')
340
+ .command('list')
341
+ .alias('ls')
342
+ .description('List registered calendars')
323
343
  .action(async () => {
324
- await removeCalendar();
344
+ await listCalendarSourcesCommand();
345
+ });
346
+ calendarCmd
347
+ .command('remove [id]')
348
+ .alias('rm')
349
+ .description('Remove a calendar by id/number/name')
350
+ .option('--all', 'Remove all calendar configuration')
351
+ .action(async (id, options) => {
352
+ await removeCalendar(id, options);
325
353
  });
326
354
  calendarCmd
327
355
  .command('show')
@@ -336,16 +364,16 @@ calendarCmd
336
364
  await syncCalendar();
337
365
  });
338
366
  calendarCmd
339
- .command('enable')
340
- .description('Enable calendar display')
341
- .action(async () => {
342
- await enableCalendar();
367
+ .command('enable [id]')
368
+ .description('Enable calendar display (all, or a specific calendar by id)')
369
+ .action(async (id) => {
370
+ await enableCalendar(id);
343
371
  });
344
372
  calendarCmd
345
- .command('disable')
346
- .description('Disable calendar display')
347
- .action(async () => {
348
- await disableCalendar();
373
+ .command('disable [id]')
374
+ .description('Disable calendar display (all, or a specific calendar by id)')
375
+ .action(async (id) => {
376
+ await disableCalendar(id);
349
377
  });
350
378
  // OAuth commands
351
379
  calendarCmd
@@ -380,7 +408,7 @@ calendarCmd
380
408
  });
381
409
  calendarCmd
382
410
  .command('select')
383
- .description('Select a calendar to display')
411
+ .description('Select Google calendars to register (multiple supported)')
384
412
  .action(async () => {
385
413
  await selectCalendar();
386
414
  });
@@ -1,13 +1,19 @@
1
1
  /**
2
- * Add or update calendar URL (iCal mode)
2
+ * Add a new iCal calendar (multiple calendars can be registered)
3
3
  */
4
4
  export declare function addCalendar(url: string, options: {
5
5
  name?: string;
6
6
  }): Promise<void>;
7
7
  /**
8
- * Remove calendar configuration
8
+ * List registered calendars
9
9
  */
10
- export declare function removeCalendar(): Promise<void>;
10
+ export declare function listCalendarSourcesCommand(): Promise<void>;
11
+ /**
12
+ * Remove a calendar (by id/index/name), or all calendar configuration with --all
13
+ */
14
+ export declare function removeCalendar(idOrIndex?: string, options?: {
15
+ all?: boolean;
16
+ }): Promise<void>;
11
17
  /**
12
18
  * Show calendar configuration and today's events
13
19
  */
@@ -16,14 +22,8 @@ export declare function showCalendar(): Promise<void>;
16
22
  * Sync/refresh calendar cache
17
23
  */
18
24
  export declare function syncCalendar(): Promise<void>;
19
- /**
20
- * Enable calendar display
21
- */
22
- export declare function enableCalendar(): Promise<void>;
23
- /**
24
- * Disable calendar display
25
- */
26
- export declare function disableCalendar(): Promise<void>;
25
+ export declare function enableCalendar(idOrIndex?: string): Promise<void>;
26
+ export declare function disableCalendar(idOrIndex?: string): Promise<void>;
27
27
  /**
28
28
  * Configure OAuth client credentials
29
29
  */
@@ -37,6 +37,6 @@ export declare function loginCalendar(): Promise<void>;
37
37
  */
38
38
  export declare function logoutCalendar(): Promise<void>;
39
39
  /**
40
- * Select a calendar from the user's calendars
40
+ * Select Google calendars to register (multiple selections supported)
41
41
  */
42
42
  export declare function selectCalendar(): Promise<void>;