floq 1.5.0 → 1.7.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
@@ -1,5 +1,9 @@
1
1
  # Floq
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/floq.svg)](https://www.npmjs.com/package/floq)
4
+ [![npm downloads](https://img.shields.io/npm/dm/floq.svg)](https://www.npmjs.com/package/floq)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  [English](./README.md)
4
8
 
5
9
  MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getting Things Done)タスクマネージャー。
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Floq
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/floq.svg)](https://www.npmjs.com/package/floq)
4
+ [![npm downloads](https://img.shields.io/npm/dm/floq.svg)](https://www.npmjs.com/package/floq)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  [日本語](./README.ja.md)
4
8
 
5
9
  A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes.
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { listTasks, listProjects } from './commands/list.js';
7
7
  import { moveTask } from './commands/move.js';
8
8
  import { markDone } from './commands/done.js';
9
9
  import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
10
- import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
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';
@@ -153,8 +153,12 @@ configCmd
153
153
  .option('--disable', 'Temporarily disable Turso sync (preserves config)')
154
154
  .option('--enable', 'Re-enable Turso sync')
155
155
  .option('--clear', 'Remove Turso configuration completely')
156
+ .option('--qr', 'Display Turso config as QR code')
156
157
  .action(async (options) => {
157
- if (options.clear) {
158
+ if (options.qr) {
159
+ await showTursoQr();
160
+ }
161
+ else if (options.clear) {
158
162
  await clearTurso();
159
163
  }
160
164
  else if (options.disable) {
@@ -196,6 +200,24 @@ configCmd
196
200
  await showDateFormatCommand();
197
201
  }
198
202
  });
203
+ configCmd
204
+ .command('insights-weeks [weeks]')
205
+ .description('Set number of weeks for insights (default: 2)')
206
+ .action(async (weeks) => {
207
+ const { getInsightsWeeks, setInsightsWeeks } = await import('./config.js');
208
+ if (weeks !== undefined) {
209
+ const n = parseInt(weeks, 10);
210
+ if (isNaN(n) || n < 1) {
211
+ console.error('Weeks must be a positive integer');
212
+ process.exit(1);
213
+ }
214
+ setInsightsWeeks(n);
215
+ console.log(`Insights weeks set to ${n}`);
216
+ }
217
+ else {
218
+ console.log(`Insights weeks: ${getInsightsWeeks()}`);
219
+ }
220
+ });
199
221
  configCmd
200
222
  .command('pomodoro')
201
223
  .description('Configure pomodoro settings')
@@ -216,9 +238,13 @@ configCmd
216
238
  program
217
239
  .command('insights')
218
240
  .description('Show task completion insights and statistics')
219
- .option('-w, --weeks <n>', 'Number of weeks to analyze (default: 2)', '2')
241
+ .option('-w, --weeks <n>', 'Number of weeks to analyze (uses config default)')
220
242
  .action(async (options) => {
221
- const weeks = Math.max(1, parseInt(options.weeks ?? '2', 10) || 2);
243
+ const { getInsightsWeeks } = await import('./config.js');
244
+ const defaultWeeks = getInsightsWeeks();
245
+ const weeks = options.weeks
246
+ ? Math.max(1, parseInt(options.weeks, 10) || defaultWeeks)
247
+ : defaultWeeks;
222
248
  await showInsights(weeks);
223
249
  });
224
250
  // Sync command
@@ -358,4 +384,12 @@ calendarCmd
358
384
  .action(async () => {
359
385
  await selectCalendar();
360
386
  });
387
+ // MCP server command
388
+ program
389
+ .command('mcp')
390
+ .description('Start MCP server for LLM integration')
391
+ .action(async () => {
392
+ const { startMcpServer } = await import('./mcp/server.js');
393
+ await startMcpServer();
394
+ });
361
395
  export { program };
@@ -7,6 +7,7 @@ export declare function selectTheme(): Promise<void>;
7
7
  export declare function showViewMode(): Promise<void>;
8
8
  export declare function setViewModeCommand(mode: string): Promise<void>;
9
9
  export declare function selectMode(): Promise<void>;
10
+ export declare function showTursoQr(): Promise<void>;
10
11
  export declare function setTurso(url: string, token: string): Promise<void>;
11
12
  export declare function disableTurso(): Promise<void>;
12
13
  export declare function enableTurso(): Promise<void>;
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { createInterface } from 'readline';
4
4
  import { unlinkSync, existsSync, readdirSync } from 'fs';
5
5
  import { dirname, basename, join } from 'path';
6
- import { loadConfig, saveConfig, getDbPath, getViewMode, setViewMode, isTursoEnabled, getTursoConfig, setTursoConfig, setTursoEnabled, getSplashDuration, setSplashDuration, getDateFormat, setDateFormat, getCalendarConfig, isCalendarEnabled } from '../config.js';
6
+ import { loadConfig, saveConfig, getDbPath, getViewMode, setViewMode, isTursoEnabled, getTursoConfig, setTursoConfig, setTursoEnabled, getSplashDuration, setSplashDuration, getDateFormat, setDateFormat, getCalendarConfig, isCalendarEnabled, getInsightsWeeks } from '../config.js';
7
7
  import { CONFIG_FILE } from '../paths.js';
8
8
  import { ThemeSelector } from '../ui/ThemeSelector.js';
9
9
  import { ModeSelector } from '../ui/ModeSelector.js';
@@ -25,6 +25,7 @@ export async function showConfig() {
25
25
  console.log(`View Mode: ${config.viewMode || 'gtd'}`);
26
26
  console.log(`Date Format: ${dateFormat}`);
27
27
  console.log(`Splash: ${splashDuration === 0 ? 'disabled' : splashDuration === -1 ? 'wait for key' : `${splashDuration}ms`}`);
28
+ console.log(`Insights Weeks: ${getInsightsWeeks()}`);
28
29
  console.log(`Turso: ${isTursoEnabled() ? 'enabled' : 'disabled'}`);
29
30
  if (config.db_path) {
30
31
  console.log(` (custom: ${config.db_path})`);
@@ -126,6 +127,22 @@ export async function selectMode() {
126
127
  }));
127
128
  });
128
129
  }
130
+ export async function showTursoQr() {
131
+ const turso = getTursoConfig();
132
+ if (!turso || !turso.url || !turso.authToken) {
133
+ console.error('Turso is not configured.');
134
+ console.error('Use "floq config turso --url <url> --token <token>" to configure first.');
135
+ process.exit(1);
136
+ }
137
+ const qrcode = await import('qrcode');
138
+ const data = JSON.stringify({ url: turso.url, authToken: turso.authToken });
139
+ const qr = await qrcode.default.toString(data, { type: 'terminal', small: true });
140
+ console.log('');
141
+ console.log('Turso Configuration QR Code:');
142
+ console.log('');
143
+ console.log(qr);
144
+ console.log('⚠️ This QR code contains your auth token. Do not share publicly.');
145
+ }
129
146
  export async function setTurso(url, token) {
130
147
  setTursoConfig({ url, authToken: token, enabled: true });
131
148
  console.log('Turso sync enabled');
package/dist/config.d.ts CHANGED
@@ -43,6 +43,7 @@ export interface Config {
43
43
  pomodoroFocusMode?: boolean;
44
44
  dateFormat?: DateFormat;
45
45
  calendar?: CalendarConfig;
46
+ insightsWeeks?: number;
46
47
  }
47
48
  export declare function loadConfig(): Config;
48
49
  export declare function saveConfig(updates: Partial<Config>): void;
@@ -80,3 +81,5 @@ export declare function setGoogleOAuthClient(client: GoogleOAuthClient): void;
80
81
  export declare function getCalendarOAuthConfig(): CalendarOAuthConfig | undefined;
81
82
  export declare function setCalendarOAuthConfig(oauth: CalendarOAuthConfig | undefined): void;
82
83
  export declare function getCalendarType(): 'ical' | 'oauth' | undefined;
84
+ export declare function getInsightsWeeks(): number;
85
+ export declare function setInsightsWeeks(weeks: number): void;
package/dist/config.js CHANGED
@@ -244,3 +244,14 @@ export function getCalendarType() {
244
244
  return undefined;
245
245
  return calendar.type || (calendar.url ? 'ical' : undefined);
246
246
  }
247
+ const DEFAULT_INSIGHTS_WEEKS = 2;
248
+ export function getInsightsWeeks() {
249
+ const weeks = loadConfig().insightsWeeks;
250
+ if (weeks === undefined || weeks < 1) {
251
+ return DEFAULT_INSIGHTS_WEEKS;
252
+ }
253
+ return weeks;
254
+ }
255
+ export function setInsightsWeeks(weeks) {
256
+ saveConfig({ insightsWeeks: Math.max(1, weeks) });
257
+ }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ async function main() {
10
10
  const isConfigCommand = args[0] === 'config';
11
11
  const isSyncCommand = args[0] === 'sync';
12
12
  const isSetupCommand = args[0] === 'setup';
13
+ const isMcpCommand = args[0] === 'mcp';
13
14
  // 初回起動時(引数なし + config未作成)はウィザードを起動
14
15
  if (isTuiMode && isFirstRun()) {
15
16
  await runSetupWizard();
@@ -19,7 +20,7 @@ async function main() {
19
20
  return;
20
21
  }
21
22
  // config/syncコマンド以外でTursoモードの場合は接続先を表示
22
- if (!isTuiMode && !isConfigCommand && !isSyncCommand && !isSetupCommand && isTursoEnabled()) {
23
+ if (!isTuiMode && !isConfigCommand && !isSyncCommand && !isSetupCommand && !isMcpCommand && isTursoEnabled()) {
23
24
  const turso = getTursoConfig();
24
25
  if (turso) {
25
26
  const host = new URL(turso.url).host;
@@ -0,0 +1 @@
1
+ export declare function startMcpServer(): Promise<void>;
@@ -0,0 +1,186 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import { eq, like, ne, and } from 'drizzle-orm';
6
+ import { getDb, schema } from '../db/index.js';
7
+ import { VERSION } from '../version.js';
8
+ export async function startMcpServer() {
9
+ const server = new McpServer({
10
+ name: 'floq',
11
+ version: VERSION,
12
+ });
13
+ // floq_add_task
14
+ server.tool('floq_add_task', 'Add a new task to Floq. By default, tasks are added to the inbox.', {
15
+ title: z.string().describe('Task title'),
16
+ description: z.string().optional().describe('Task description'),
17
+ status: z.enum(['inbox', 'next', 'waiting', 'someday']).optional().describe('Initial status (default: inbox)'),
18
+ context: z.string().optional().describe('Task context (e.g., work, home)'),
19
+ }, async ({ title, description, status, context }) => {
20
+ const db = getDb();
21
+ const now = new Date();
22
+ const id = uuidv4();
23
+ await db.insert(schema.tasks).values({
24
+ id,
25
+ title,
26
+ description: description ?? null,
27
+ status: status ?? 'inbox',
28
+ context: context?.toLowerCase().replace(/^@/, '') ?? null,
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ });
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: JSON.stringify({ id, title, status: status ?? 'inbox' }),
36
+ }],
37
+ };
38
+ });
39
+ // floq_list_tasks
40
+ server.tool('floq_list_tasks', 'List tasks from Floq. Use status filter to narrow results. "all" returns all non-done tasks.', {
41
+ status: z.enum(['inbox', 'next', 'waiting', 'someday', 'done', 'all']).optional().describe('Filter by status (default: all, which excludes done)'),
42
+ }, async ({ status }) => {
43
+ const db = getDb();
44
+ const filter = status ?? 'all';
45
+ let tasks;
46
+ if (filter === 'all') {
47
+ tasks = await db
48
+ .select()
49
+ .from(schema.tasks)
50
+ .where(and(ne(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
51
+ }
52
+ else {
53
+ tasks = await db
54
+ .select()
55
+ .from(schema.tasks)
56
+ .where(and(eq(schema.tasks.status, filter), eq(schema.tasks.isProject, false)));
57
+ }
58
+ const result = tasks.map(task => ({
59
+ id: task.id,
60
+ shortId: task.id.slice(0, 8),
61
+ title: task.title,
62
+ description: task.description,
63
+ status: task.status,
64
+ context: task.context,
65
+ waitingFor: task.waitingFor,
66
+ dueDate: task.dueDate?.toISOString() ?? null,
67
+ createdAt: task.createdAt.toISOString(),
68
+ }));
69
+ return {
70
+ content: [{
71
+ type: 'text',
72
+ text: JSON.stringify(result),
73
+ }],
74
+ };
75
+ });
76
+ // floq_complete_task
77
+ server.tool('floq_complete_task', 'Mark a task as done. Accepts full ID or ID prefix (first 8 chars).', {
78
+ taskId: z.string().describe('Task ID or ID prefix'),
79
+ }, async ({ taskId }) => {
80
+ const db = getDb();
81
+ const tasks = await db
82
+ .select()
83
+ .from(schema.tasks)
84
+ .where(like(schema.tasks.id, `${taskId}%`));
85
+ if (tasks.length === 0) {
86
+ return {
87
+ content: [{
88
+ type: 'text',
89
+ text: JSON.stringify({ error: `No task found with ID prefix: ${taskId}` }),
90
+ }],
91
+ isError: true,
92
+ };
93
+ }
94
+ if (tasks.length > 1) {
95
+ const matches = tasks.map(t => ({ id: t.id.slice(0, 8), title: t.title }));
96
+ return {
97
+ content: [{
98
+ type: 'text',
99
+ text: JSON.stringify({ error: 'Multiple tasks match this prefix', matches }),
100
+ }],
101
+ isError: true,
102
+ };
103
+ }
104
+ const task = tasks[0];
105
+ if (task.status === 'done') {
106
+ return {
107
+ content: [{
108
+ type: 'text',
109
+ text: JSON.stringify({ id: task.id, title: task.title, status: 'done', message: 'Task is already done' }),
110
+ }],
111
+ };
112
+ }
113
+ const previousStatus = task.status;
114
+ await db.update(schema.tasks)
115
+ .set({
116
+ status: 'done',
117
+ completedAt: new Date(),
118
+ updatedAt: new Date(),
119
+ })
120
+ .where(eq(schema.tasks.id, task.id));
121
+ return {
122
+ content: [{
123
+ type: 'text',
124
+ text: JSON.stringify({ id: task.id, title: task.title, previousStatus, status: 'done' }),
125
+ }],
126
+ };
127
+ });
128
+ // floq_move_task
129
+ server.tool('floq_move_task', 'Move a task to a different status. Accepts full ID or ID prefix.', {
130
+ taskId: z.string().describe('Task ID or ID prefix'),
131
+ status: z.enum(['inbox', 'next', 'waiting', 'someday', 'done']).describe('Target status'),
132
+ waitingFor: z.string().optional().describe('Who/what the task is waiting for (required when status is "waiting")'),
133
+ }, async ({ taskId, status, waitingFor }) => {
134
+ const db = getDb();
135
+ if (status === 'waiting' && !waitingFor) {
136
+ return {
137
+ content: [{
138
+ type: 'text',
139
+ text: JSON.stringify({ error: 'waitingFor is required when status is "waiting"' }),
140
+ }],
141
+ isError: true,
142
+ };
143
+ }
144
+ const tasks = await db
145
+ .select()
146
+ .from(schema.tasks)
147
+ .where(like(schema.tasks.id, `${taskId}%`));
148
+ if (tasks.length === 0) {
149
+ return {
150
+ content: [{
151
+ type: 'text',
152
+ text: JSON.stringify({ error: `No task found with ID prefix: ${taskId}` }),
153
+ }],
154
+ isError: true,
155
+ };
156
+ }
157
+ if (tasks.length > 1) {
158
+ const matches = tasks.map(t => ({ id: t.id.slice(0, 8), title: t.title }));
159
+ return {
160
+ content: [{
161
+ type: 'text',
162
+ text: JSON.stringify({ error: 'Multiple tasks match this prefix', matches }),
163
+ }],
164
+ isError: true,
165
+ };
166
+ }
167
+ const task = tasks[0];
168
+ const previousStatus = task.status;
169
+ await db.update(schema.tasks)
170
+ .set({
171
+ status,
172
+ waitingFor: status === 'waiting' ? (waitingFor ?? null) : null,
173
+ completedAt: status === 'done' ? new Date() : null,
174
+ updatedAt: new Date(),
175
+ })
176
+ .where(eq(schema.tasks.id, task.id));
177
+ return {
178
+ content: [{
179
+ type: 'text',
180
+ text: JSON.stringify({ id: task.id, title: task.title, previousStatus, status }),
181
+ }],
182
+ };
183
+ });
184
+ const transport = new StdioServerTransport();
185
+ await server.connect(transport);
186
+ }
@@ -4,6 +4,7 @@ import { Box, Text, useInput } from 'ink';
4
4
  import { eq, and } from 'drizzle-orm';
5
5
  import { getDb, schema } from '../../db/index.js';
6
6
  import { t, fmt } from '../../i18n/index.js';
7
+ import { getInsightsWeeks } from '../../config.js';
7
8
  import { useTheme } from '../theme/index.js';
8
9
  const VISIBLE_LINES = 16;
9
10
  function stringWidth(str) {
@@ -75,7 +76,7 @@ export function InsightsModal({ onClose }) {
75
76
  useEffect(() => {
76
77
  const loadInsights = async () => {
77
78
  const db = getDb();
78
- const weeks = 2;
79
+ const weeks = getInsightsWeeks();
79
80
  const lines = [];
80
81
  const now = new Date();
81
82
  const startDate = getWeekStart(now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,20 +39,23 @@
39
39
  "dependencies": {
40
40
  "@inkjs/ui": "^2.0.0",
41
41
  "@libsql/client": "^0.17.0",
42
+ "@modelcontextprotocol/sdk": "^1.27.0",
42
43
  "better-sqlite3": "^11.7.0",
43
44
  "commander": "^13.1.0",
44
45
  "drizzle-orm": "^0.39.1",
46
+ "googleapis": "^144.0.0",
47
+ "ical.js": "^2.1.0",
45
48
  "ink": "^5.1.0",
46
49
  "ink-text-input": "^6.0.0",
50
+ "open": "^10.1.0",
51
+ "qrcode": "^1.5.4",
47
52
  "react": "^18.3.1",
48
- "uuid": "^11.0.5",
49
- "ical.js": "^2.1.0",
50
- "googleapis": "^144.0.0",
51
- "open": "^10.1.0"
53
+ "uuid": "^11.0.5"
52
54
  },
53
55
  "devDependencies": {
54
56
  "@types/better-sqlite3": "^7.6.12",
55
57
  "@types/node": "^22.10.7",
58
+ "@types/qrcode": "^1.5.6",
56
59
  "@types/react": "^18.3.18",
57
60
  "@types/uuid": "^10.0.0",
58
61
  "drizzle-kit": "^0.31.8",