floq 1.5.0 → 1.6.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
@@ -196,6 +196,24 @@ configCmd
196
196
  await showDateFormatCommand();
197
197
  }
198
198
  });
199
+ configCmd
200
+ .command('insights-weeks [weeks]')
201
+ .description('Set number of weeks for insights (default: 2)')
202
+ .action(async (weeks) => {
203
+ const { getInsightsWeeks, setInsightsWeeks } = await import('./config.js');
204
+ if (weeks !== undefined) {
205
+ const n = parseInt(weeks, 10);
206
+ if (isNaN(n) || n < 1) {
207
+ console.error('Weeks must be a positive integer');
208
+ process.exit(1);
209
+ }
210
+ setInsightsWeeks(n);
211
+ console.log(`Insights weeks set to ${n}`);
212
+ }
213
+ else {
214
+ console.log(`Insights weeks: ${getInsightsWeeks()}`);
215
+ }
216
+ });
199
217
  configCmd
200
218
  .command('pomodoro')
201
219
  .description('Configure pomodoro settings')
@@ -216,9 +234,13 @@ configCmd
216
234
  program
217
235
  .command('insights')
218
236
  .description('Show task completion insights and statistics')
219
- .option('-w, --weeks <n>', 'Number of weeks to analyze (default: 2)', '2')
237
+ .option('-w, --weeks <n>', 'Number of weeks to analyze (uses config default)')
220
238
  .action(async (options) => {
221
- const weeks = Math.max(1, parseInt(options.weeks ?? '2', 10) || 2);
239
+ const { getInsightsWeeks } = await import('./config.js');
240
+ const defaultWeeks = getInsightsWeeks();
241
+ const weeks = options.weeks
242
+ ? Math.max(1, parseInt(options.weeks, 10) || defaultWeeks)
243
+ : defaultWeeks;
222
244
  await showInsights(weeks);
223
245
  });
224
246
  // Sync command
@@ -358,4 +380,12 @@ calendarCmd
358
380
  .action(async () => {
359
381
  await selectCalendar();
360
382
  });
383
+ // MCP server command
384
+ program
385
+ .command('mcp')
386
+ .description('Start MCP server for LLM integration')
387
+ .action(async () => {
388
+ const { startMcpServer } = await import('./mcp/server.js');
389
+ await startMcpServer();
390
+ });
361
391
  export { program };
@@ -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})`);
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.6.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,16 +39,17 @@
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",
47
51
  "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"
52
+ "uuid": "^11.0.5"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/better-sqlite3": "^7.6.12",