floq 1.3.2 → 1.4.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
@@ -13,6 +13,8 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
13
13
  - **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
14
14
  - **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
15
15
  - **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング。タスク追加時は現在のフィルターを自動継承
16
+ - **集中モード**: タスクに「今日やる」マーク(★)を付け、集中タスクだけに絞り込み表示
17
+ - **作業量**: タスクに作業量(S/M/L)を設定し、空き時間に合ったタスクを選びやすく
16
18
  - **タスク検索**: `/` キーで全タスクを素早く検索
17
19
  - **コメント**: タスクにメモやコメントを追加
18
20
  - **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
@@ -64,6 +66,9 @@ floq
64
66
  | `P` | プロジェクトに紐付け |
65
67
  | `c` | コンテキスト設定 |
66
68
  | `@` | コンテキストでフィルター |
69
+ | `g` | 選択タスクの集中マーク(★)ON/OFF |
70
+ | `G` | 集中フィルター切替(集中タスクのみ表示) |
71
+ | `E` | 作業量設定(S/M/L) |
67
72
  | `Enter` | タスク詳細を開く / プロジェクトを開く |
68
73
  | `Esc/b` | 戻る |
69
74
  | `/` | タスク検索 |
@@ -115,6 +120,9 @@ floq
115
120
  | `Backspace` | タスクを左に移動(←) |
116
121
  | `c` | コンテキスト設定 |
117
122
  | `@` | コンテキストでフィルター |
123
+ | `g` | 選択タスクの集中マーク(★)ON/OFF |
124
+ | `G` | 集中フィルター切替(集中タスクのみ表示) |
125
+ | `E` | 作業量設定(S/M/L) |
118
126
  | `Enter` | タスク詳細を開く |
119
127
  | `/` | タスク検索 |
120
128
  | `r` | 更新 |
package/README.md CHANGED
@@ -13,6 +13,8 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
13
13
  - **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
14
14
  - **Projects**: Organize tasks into projects with progress tracking
15
15
  - **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context. New tasks inherit the active context filter
16
+ - **Focus Mode**: Mark tasks as "today's focus" (★) and filter to show only focused tasks
17
+ - **Effort Size**: Tag tasks with effort size (S/M/L) to pick the right task for your available time
16
18
  - **Task Search**: Quick search across all tasks with `/`
17
19
  - **Comments**: Add notes and comments to tasks
18
20
  - **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
@@ -64,6 +66,9 @@ floq
64
66
  | `P` | Link to project |
65
67
  | `c` | Set context |
66
68
  | `@` | Filter by context |
69
+ | `g` | Toggle focus (★) on selected task |
70
+ | `G` | Toggle focus filter (show only focused tasks) |
71
+ | `E` | Set effort size (S/M/L) |
67
72
  | `Enter` | Open task detail / Open project |
68
73
  | `Esc/b` | Back |
69
74
  | `/` | Search tasks |
@@ -115,6 +120,9 @@ floq
115
120
  | `Backspace` | Move task left (←) |
116
121
  | `c` | Set context |
117
122
  | `@` | Filter by context |
123
+ | `g` | Toggle focus (★) on selected task |
124
+ | `G` | Toggle focus filter (show only focused tasks) |
125
+ | `E` | Set effort size (S/M/L) |
118
126
  | `Enter` | Open task detail |
119
127
  | `/` | Search tasks |
120
128
  | `r` | Refresh |
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { addProject, listProjectsCommand, showProject, completeProject, } from '
10
10
  import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, 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
+ import { showInsights } from './commands/insights.js';
13
14
  import { runSetupWizard } from './commands/setup.js';
14
15
  import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar } from './commands/calendar.js';
15
16
  import { VERSION } from './version.js';
@@ -211,6 +212,15 @@ configCmd
211
212
  console.log(`Pomodoro focus mode: ${enabled ? 'on' : 'off'}`);
212
213
  }
213
214
  });
215
+ // Insights command
216
+ program
217
+ .command('insights')
218
+ .description('Show task completion insights and statistics')
219
+ .option('-w, --weeks <n>', 'Number of weeks to analyze (default: 2)', '2')
220
+ .action(async (options) => {
221
+ const weeks = Math.max(1, parseInt(options.weeks ?? '2', 10) || 2);
222
+ await showInsights(weeks);
223
+ });
214
224
  // Sync command
215
225
  program
216
226
  .command('sync')
@@ -0,0 +1 @@
1
+ export declare function showInsights(weeks: number): Promise<void>;
@@ -0,0 +1,283 @@
1
+ import { eq, and, gte } from 'drizzle-orm';
2
+ import { getDb, schema } from '../db/index.js';
3
+ import { t, fmt } from '../i18n/index.js';
4
+ function getWeekStart(date) {
5
+ const d = new Date(date);
6
+ const day = d.getDay();
7
+ d.setDate(d.getDate() - day);
8
+ d.setHours(0, 0, 0, 0);
9
+ return d;
10
+ }
11
+ function formatDate(date) {
12
+ return date.toLocaleDateString();
13
+ }
14
+ function bar(count, max, width = 20) {
15
+ if (max === 0)
16
+ return '';
17
+ const filled = Math.round((count / max) * width);
18
+ return '█'.repeat(filled);
19
+ }
20
+ function stringWidth(str) {
21
+ let width = 0;
22
+ for (const char of str) {
23
+ const code = char.codePointAt(0) || 0;
24
+ // CJK characters and fullwidth forms take 2 columns
25
+ if ((code >= 0x1100 && code <= 0x115F) ||
26
+ (code >= 0x2E80 && code <= 0x303E) ||
27
+ (code >= 0x3040 && code <= 0x9FFF) ||
28
+ (code >= 0xAC00 && code <= 0xD7AF) ||
29
+ (code >= 0xF900 && code <= 0xFAFF) ||
30
+ (code >= 0xFE30 && code <= 0xFE6F) ||
31
+ (code >= 0xFF01 && code <= 0xFF60) ||
32
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
33
+ (code >= 0x20000 && code <= 0x2FFFF)) {
34
+ width += 2;
35
+ }
36
+ else {
37
+ width += 1;
38
+ }
39
+ }
40
+ return width;
41
+ }
42
+ function padEnd(str, targetWidth) {
43
+ const currentWidth = stringWidth(str);
44
+ const padding = Math.max(0, targetWidth - currentWidth);
45
+ return str + ' '.repeat(padding);
46
+ }
47
+ function groupByWeek(tasks, weeks) {
48
+ const now = new Date();
49
+ const result = [];
50
+ for (let i = 0; i < weeks; i++) {
51
+ const weekStart = getWeekStart(now);
52
+ weekStart.setDate(weekStart.getDate() - i * 7);
53
+ const weekEnd = new Date(weekStart);
54
+ weekEnd.setDate(weekEnd.getDate() + 7);
55
+ const weekTasks = tasks.filter(task => {
56
+ const updated = task.updatedAt;
57
+ return updated >= weekStart && updated < weekEnd;
58
+ });
59
+ result.push({
60
+ weekStart,
61
+ weekLabel: formatDate(weekStart),
62
+ tasks: weekTasks,
63
+ });
64
+ }
65
+ return result;
66
+ }
67
+ function groupByDayOfWeek(tasks) {
68
+ const days = new Map();
69
+ for (let i = 0; i < 7; i++)
70
+ days.set(i, 0);
71
+ for (const task of tasks) {
72
+ const day = task.updatedAt.getDay();
73
+ days.set(day, (days.get(day) || 0) + 1);
74
+ }
75
+ return days;
76
+ }
77
+ function calculateDistribution(tasks, getter, noValueLabel) {
78
+ const map = new Map();
79
+ for (const task of tasks) {
80
+ const key = getter(task) || noValueLabel;
81
+ map.set(key, (map.get(key) || 0) + 1);
82
+ }
83
+ return Array.from(map.entries())
84
+ .map(([label, count]) => ({
85
+ label,
86
+ count,
87
+ percentage: Math.round((count / tasks.length) * 100),
88
+ }))
89
+ .sort((a, b) => b.count - a.count);
90
+ }
91
+ function calculateAverageCompletionDays(tasks) {
92
+ if (tasks.length === 0)
93
+ return null;
94
+ let totalMs = 0;
95
+ let validCount = 0;
96
+ for (const task of tasks) {
97
+ const created = task.createdAt.getTime();
98
+ const updated = task.updatedAt.getTime();
99
+ if (updated > created) {
100
+ totalMs += updated - created;
101
+ validCount++;
102
+ }
103
+ }
104
+ if (validCount === 0)
105
+ return null;
106
+ return totalMs / validCount / (1000 * 60 * 60 * 24);
107
+ }
108
+ export async function showInsights(weeks) {
109
+ const db = getDb();
110
+ const i18n = t();
111
+ const ins = i18n.commands.insights;
112
+ const isJa = ins?.title === 'タスクインサイト';
113
+ // Labels with fallbacks
114
+ const l = {
115
+ title: ins?.title || 'Task Insights',
116
+ period: ins?.period || 'Period',
117
+ weeklyCompletion: ins?.weeklyCompletion || 'Weekly Completion',
118
+ weekLabel: ins?.weekLabel || 'Week of {date}',
119
+ tasksCompleted: ins?.tasksCompleted || '{count} tasks completed',
120
+ dailyBreakdown: ins?.dailyBreakdown || 'Daily Breakdown',
121
+ currentStatus: ins?.currentStatus || 'Current Status',
122
+ byContext: ins?.byContext || 'By Context',
123
+ byEffort: ins?.byEffort || 'By Effort',
124
+ noContext: ins?.noContext || 'No context',
125
+ noEffort: ins?.noEffort || 'No effort set',
126
+ projectProgress: ins?.projectProgress || 'Project Progress',
127
+ activeProjects: ins?.activeProjects || 'Active Projects',
128
+ tasksRemaining: ins?.tasksRemaining || '{count} tasks remaining',
129
+ averageCompletion: ins?.averageCompletion || 'Average Completion Time',
130
+ daysAverage: ins?.daysAverage || '{days} days',
131
+ noData: ins?.noData || 'No completed tasks in this period',
132
+ total: ins?.total || 'Total',
133
+ andMore: ins?.andMore || ' ... and {count} more',
134
+ };
135
+ // Calculate date range
136
+ const now = new Date();
137
+ const startDate = getWeekStart(now);
138
+ startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
139
+ const endDate = new Date(now);
140
+ endDate.setHours(23, 59, 59, 999);
141
+ // Query all completed tasks in the period
142
+ const completedTasks = await db
143
+ .select()
144
+ .from(schema.tasks)
145
+ .where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
146
+ // Header
147
+ console.log();
148
+ console.log(l.title);
149
+ console.log('═'.repeat(40));
150
+ console.log(`${l.period}: ${formatDate(startDate)} ~ ${formatDate(endDate)}`);
151
+ if (completedTasks.length === 0) {
152
+ console.log();
153
+ console.log(` ${l.noData}`);
154
+ console.log();
155
+ return;
156
+ }
157
+ // Weekly Completion
158
+ console.log();
159
+ console.log(l.weeklyCompletion);
160
+ console.log('─'.repeat(40));
161
+ const weeklyStats = groupByWeek(completedTasks, weeks);
162
+ for (const week of weeklyStats) {
163
+ const weekLabel = fmt(l.weekLabel, { date: week.weekLabel });
164
+ const countLabel = fmt(l.tasksCompleted, { count: week.tasks.length });
165
+ console.log(`${weekLabel}: ${countLabel}`);
166
+ const maxShow = 10;
167
+ for (let i = 0; i < Math.min(week.tasks.length, maxShow); i++) {
168
+ const task = week.tasks[i];
169
+ const shortId = task.id.slice(0, 8);
170
+ console.log(` [${shortId}] ${task.title}`);
171
+ }
172
+ if (week.tasks.length > maxShow) {
173
+ console.log(fmt(l.andMore, { count: week.tasks.length - maxShow }));
174
+ }
175
+ }
176
+ // Daily Breakdown
177
+ console.log();
178
+ console.log(l.dailyBreakdown);
179
+ console.log('─'.repeat(40));
180
+ const localeDayNames = isJa
181
+ ? ['日', '月', '火', '水', '木', '金', '土']
182
+ : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
183
+ const dailyStats = groupByDayOfWeek(completedTasks);
184
+ const maxDaily = Math.max(...dailyStats.values());
185
+ for (let i = 0; i < 7; i++) {
186
+ const count = dailyStats.get(i) || 0;
187
+ const dayName = padEnd(localeDayNames[i], 4);
188
+ const countStr = String(count).padStart(3);
189
+ console.log(` ${dayName}${countStr} ${bar(count, maxDaily)}`);
190
+ }
191
+ // Current Status Distribution
192
+ console.log();
193
+ console.log(l.currentStatus);
194
+ console.log('─'.repeat(40));
195
+ const allTasks = await db
196
+ .select()
197
+ .from(schema.tasks)
198
+ .where(eq(schema.tasks.isProject, false));
199
+ const statusCounts = new Map();
200
+ for (const task of allTasks) {
201
+ statusCounts.set(task.status, (statusCounts.get(task.status) || 0) + 1);
202
+ }
203
+ const statusOrder = ['inbox', 'next', 'waiting', 'someday', 'done'];
204
+ const maxStatus = Math.max(...statusCounts.values());
205
+ for (const status of statusOrder) {
206
+ const count = statusCounts.get(status) || 0;
207
+ const label = padEnd(i18n.status[status] || status, 16);
208
+ const countStr = String(count).padStart(3);
209
+ console.log(` ${label}${countStr} ${bar(count, maxStatus)}`);
210
+ }
211
+ // Context Distribution
212
+ console.log();
213
+ console.log(l.byContext);
214
+ console.log('─'.repeat(40));
215
+ const contextDist = calculateDistribution(completedTasks, t => t.context, l.noContext);
216
+ const maxContext = contextDist.length > 0 ? contextDist[0].count : 0;
217
+ for (const item of contextDist) {
218
+ const label = padEnd(item.label === l.noContext ? item.label : `@${item.label}`, 16);
219
+ const countStr = String(item.count).padStart(3);
220
+ console.log(` ${label}${countStr} (${String(item.percentage).padStart(2)}%) ${bar(item.count, maxContext)}`);
221
+ }
222
+ // Effort Distribution
223
+ console.log();
224
+ console.log(l.byEffort);
225
+ console.log('─'.repeat(40));
226
+ const effortLabels = {
227
+ small: i18n.tui.effort?.small || 'Small',
228
+ medium: i18n.tui.effort?.medium || 'Medium',
229
+ large: i18n.tui.effort?.large || 'Large',
230
+ };
231
+ const effortDist = calculateDistribution(completedTasks, t => t.effort ? (effortLabels[t.effort] || t.effort) : null, l.noEffort);
232
+ const maxEffort = effortDist.length > 0 ? effortDist[0].count : 0;
233
+ for (const item of effortDist) {
234
+ const label = padEnd(item.label, 16);
235
+ const countStr = String(item.count).padStart(3);
236
+ console.log(` ${label}${countStr} (${String(item.percentage).padStart(2)}%) ${bar(item.count, maxEffort)}`);
237
+ }
238
+ // Project Progress
239
+ console.log();
240
+ console.log(l.projectProgress);
241
+ console.log('─'.repeat(40));
242
+ const activeProjects = await db
243
+ .select()
244
+ .from(schema.tasks)
245
+ .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
246
+ if (activeProjects.length === 0) {
247
+ console.log(` ${l.activeProjects}: 0`);
248
+ }
249
+ else {
250
+ console.log(` ${l.activeProjects}: ${activeProjects.length}`);
251
+ for (const project of activeProjects) {
252
+ const children = await db
253
+ .select()
254
+ .from(schema.tasks)
255
+ .where(eq(schema.tasks.parentId, project.id));
256
+ const total = children.length;
257
+ const done = children.filter(c => c.status === 'done').length;
258
+ const remaining = total - done;
259
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
260
+ const shortId = project.id.slice(0, 8);
261
+ console.log(` [${shortId}] ${project.title} (${done}/${total}, ${pct}%)`);
262
+ }
263
+ }
264
+ // Average Completion Time
265
+ console.log();
266
+ console.log(l.averageCompletion);
267
+ console.log('─'.repeat(40));
268
+ const avgDays = calculateAverageCompletionDays(completedTasks);
269
+ if (avgDays !== null) {
270
+ if (avgDays < 1) {
271
+ const hours = Math.round(avgDays * 24);
272
+ const hoursLabel = isJa ? `${hours}時間` : `${hours}h`;
273
+ console.log(` ${hoursLabel}`);
274
+ }
275
+ else {
276
+ console.log(` ${fmt(l.daysAverage, { days: avgDays.toFixed(1) })}`);
277
+ }
278
+ }
279
+ // Total
280
+ console.log();
281
+ console.log(`${l.total}: ${fmt(l.tasksCompleted, { count: completedTasks.length })}`);
282
+ console.log();
283
+ }
package/dist/config.d.ts CHANGED
@@ -39,6 +39,7 @@ export interface Config {
39
39
  contexts?: string[];
40
40
  splashDuration?: number;
41
41
  contextFilter?: string | null;
42
+ focusFilter?: boolean;
42
43
  pomodoroFocusMode?: boolean;
43
44
  dateFormat?: DateFormat;
44
45
  calendar?: CalendarConfig;
@@ -64,6 +65,8 @@ export declare function getSplashDuration(): number;
64
65
  export declare function setSplashDuration(duration: number): void;
65
66
  export declare function getContextFilter(): string | null;
66
67
  export declare function setContextFilter(contextFilter: string | null): void;
68
+ export declare function getFocusFilter(): boolean;
69
+ export declare function setFocusFilter(enabled: boolean): void;
67
70
  export declare function getPomodoroFocusMode(): boolean;
68
71
  export declare function setPomodoroFocusMode(enabled: boolean): void;
69
72
  export declare function getDateFormat(): DateFormat;
package/dist/config.js CHANGED
@@ -165,6 +165,12 @@ export function getContextFilter() {
165
165
  export function setContextFilter(contextFilter) {
166
166
  saveConfig({ contextFilter });
167
167
  }
168
+ export function getFocusFilter() {
169
+ return loadConfig().focusFilter ?? false;
170
+ }
171
+ export function setFocusFilter(enabled) {
172
+ saveConfig({ focusFilter: enabled });
173
+ }
168
174
  export function getPomodoroFocusMode() {
169
175
  return loadConfig().pomodoroFocusMode ?? true;
170
176
  }
package/dist/db/index.js CHANGED
@@ -24,6 +24,8 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
24
24
  const tableInfo = tableInfoResult.rows;
25
25
  const tableExists = tableInfo.length > 0;
26
26
  const hasContext = tableInfo.some(col => col.name === 'context');
27
+ const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
28
+ const hasEffort = tableInfo.some(col => col.name === 'effort');
27
29
  if (!tableExists) {
28
30
  // Fresh install: create new schema on remote
29
31
  await remoteClient.execute(`
@@ -36,6 +38,8 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
36
38
  parent_id TEXT,
37
39
  waiting_for TEXT,
38
40
  context TEXT,
41
+ is_focused INTEGER NOT NULL DEFAULT 0,
42
+ effort TEXT,
39
43
  due_date INTEGER,
40
44
  created_at INTEGER NOT NULL,
41
45
  updated_at INTEGER NOT NULL
@@ -51,6 +55,14 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
51
55
  await remoteClient.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
52
56
  await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
53
57
  }
58
+ // Migration: add is_focused column if missing
59
+ if (tableExists && !hasIsFocused) {
60
+ await remoteClient.execute("ALTER TABLE tasks ADD COLUMN is_focused INTEGER NOT NULL DEFAULT 0");
61
+ }
62
+ // Migration: add effort column if missing
63
+ if (tableExists && !hasEffort) {
64
+ await remoteClient.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
65
+ }
54
66
  // Create comments table
55
67
  await remoteClient.execute(`
56
68
  CREATE TABLE IF NOT EXISTS comments (
@@ -88,6 +100,8 @@ async function initializeLocalSchema() {
88
100
  const hasProjectId = tableInfo.some(col => col.name === 'project_id');
89
101
  const hasIsProject = tableInfo.some(col => col.name === 'is_project');
90
102
  const hasContext = tableInfo.some(col => col.name === 'context');
103
+ const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
104
+ const hasEffort = tableInfo.some(col => col.name === 'effort');
91
105
  const tableExists = tableInfo.length > 0;
92
106
  if (tableExists && hasProjectId && !hasIsProject) {
93
107
  // Migration: old schema -> new schema
@@ -119,6 +133,8 @@ async function initializeLocalSchema() {
119
133
  parent_id TEXT,
120
134
  waiting_for TEXT,
121
135
  context TEXT,
136
+ is_focused INTEGER NOT NULL DEFAULT 0,
137
+ effort TEXT,
122
138
  due_date INTEGER,
123
139
  created_at INTEGER NOT NULL,
124
140
  updated_at INTEGER NOT NULL
@@ -134,6 +150,14 @@ async function initializeLocalSchema() {
134
150
  await client.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
135
151
  await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
136
152
  }
153
+ // Migration: add is_focused column if missing
154
+ if (tableExists && !hasIsFocused) {
155
+ await client.execute("ALTER TABLE tasks ADD COLUMN is_focused INTEGER NOT NULL DEFAULT 0");
156
+ }
157
+ // Migration: add effort column if missing
158
+ if (tableExists && !hasEffort) {
159
+ await client.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
160
+ }
137
161
  // Create comments table
138
162
  await client.execute(`
139
163
  CREATE TABLE IF NOT EXISTS comments (
@@ -152,6 +152,42 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
152
152
  }, {}, {
153
153
  length: number | undefined;
154
154
  }>;
155
+ isFocused: import("drizzle-orm/sqlite-core").SQLiteColumn<{
156
+ name: "is_focused";
157
+ tableName: "tasks";
158
+ dataType: "boolean";
159
+ columnType: "SQLiteBoolean";
160
+ data: boolean;
161
+ driverParam: number;
162
+ notNull: true;
163
+ hasDefault: true;
164
+ isPrimaryKey: false;
165
+ isAutoincrement: false;
166
+ hasRuntimeDefault: false;
167
+ enumValues: undefined;
168
+ baseColumn: never;
169
+ identity: undefined;
170
+ generated: undefined;
171
+ }, {}, {}>;
172
+ effort: import("drizzle-orm/sqlite-core").SQLiteColumn<{
173
+ name: "effort";
174
+ tableName: "tasks";
175
+ dataType: "string";
176
+ columnType: "SQLiteText";
177
+ data: string;
178
+ driverParam: string;
179
+ notNull: false;
180
+ hasDefault: false;
181
+ isPrimaryKey: false;
182
+ isAutoincrement: false;
183
+ hasRuntimeDefault: false;
184
+ enumValues: [string, ...string[]];
185
+ baseColumn: never;
186
+ identity: undefined;
187
+ generated: undefined;
188
+ }, {}, {
189
+ length: number | undefined;
190
+ }>;
155
191
  dueDate: import("drizzle-orm/sqlite-core").SQLiteColumn<{
156
192
  name: "due_date";
157
193
  tableName: "tasks";
@@ -209,6 +245,7 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
209
245
  export type Task = typeof tasks.$inferSelect;
210
246
  export type NewTask = typeof tasks.$inferInsert;
211
247
  export type TaskStatus = 'inbox' | 'next' | 'waiting' | 'someday' | 'done';
248
+ export type EffortSize = 'small' | 'medium' | 'large';
212
249
  export declare const comments: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
213
250
  name: "comments";
214
251
  schema: undefined;
package/dist/db/schema.js CHANGED
@@ -8,6 +8,8 @@ export const tasks = sqliteTable('tasks', {
8
8
  parentId: text('parent_id'),
9
9
  waitingFor: text('waiting_for'),
10
10
  context: text('context'),
11
+ isFocused: integer('is_focused', { mode: 'boolean' }).notNull().default(false),
12
+ effort: text('effort'),
11
13
  dueDate: integer('due_date', { mode: 'timestamp' }),
12
14
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
13
15
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),