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 +4 -0
- package/README.md +4 -0
- package/dist/cli.js +32 -2
- package/dist/commands/config.js +2 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/index.js +2 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +186 -0
- package/dist/ui/components/InsightsModal.js +2 -1
- package/package.json +6 -5
package/README.ja.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Floq
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/floq)
|
|
4
|
+
[](https://www.npmjs.com/package/floq)
|
|
5
|
+
[](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
|
+
[](https://www.npmjs.com/package/floq)
|
|
4
|
+
[](https://www.npmjs.com/package/floq)
|
|
5
|
+
[](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
|
|
237
|
+
.option('-w, --weeks <n>', 'Number of weeks to analyze (uses config default)')
|
|
220
238
|
.action(async (options) => {
|
|
221
|
-
const
|
|
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 };
|
package/dist/commands/config.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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",
|