Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Documentation

on:
push:
branches:
- main
paths:
- 'docs/**'
- 'zensical.toml'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
- 'zensical.toml'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/configure-pages@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.x'
# 0.0.x のため固定。更新時はローカルで zensical build を通してから上げる
- run: pip install zensical==0.0.74
- run: zensical build --clean --strict
- uses: actions/upload-pages-artifact@v5
with:
path: site

# 公開は main への push のみ(PRではビルド確認だけ)
deploy:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v5
id: deployment
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ dist-ssr
*.sln
*.sw?
.claude/worktrees/
docs/
CLAUDE.md

# Zensical のビルド出力
site/
113 changes: 113 additions & 0 deletions docs/google-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Google カレンダー / Google ToDo 連携セットアップ

FastTask のタスクを **Google へ書き出す(push)だけ** の一方向連携です。
Google 側で作ったり編集したりした予定・ToDo が FastTask に取り込まれることはありません。

アプリにはクライアントシークレットを埋め込んでいないため、**各自の Google Cloud プロジェクトで
OAuth クライアントIDを発行**して設定画面に入力する必要があります。以下はその手順です。

---

## 1. Google Cloud プロジェクトを作る

1. <https://console.cloud.google.com/> を開く
2. 画面上部のプロジェクト選択 → 「新しいプロジェクト」
3. プロジェクト名は何でもよい(例: `FastTask`)→ 作成

## 2. API を有効化する

左メニュー「API とサービス」→「ライブラリ」から、次の2つを検索して **有効にする**。

- **Google Calendar API**
- **Google Tasks API**

## 3. OAuth 同意画面を設定する

左メニュー「API とサービス」→「OAuth 同意画面」。

1. User Type は **外部(External)** を選択(Google Workspace 組織内だけで使うなら「内部」でも可)
2. アプリ名(例: `FastTask`)、ユーザーサポートメール、デベロッパー連絡先メールを入力
3. **スコープ**: ここでは何も追加しなくて構いません(アプリ側が要求します)。
FastTask が要求するスコープは最小限の3つだけです。
- `.../auth/calendar.events` … カレンダーの予定の作成・更新
- `.../auth/tasks` … ToDo の作成・更新
- `.../auth/userinfo.email` … 設定画面に「接続中のアカウント」を表示するためだけに使用
4. **テストユーザー**: 「公開ステータス」が *テスト* のままで問題ありません。
「テストユーザー」に **自分の Google アカウントのメールアドレスを必ず追加**してください。
ここに登録していないアカウントでは認証が `access_denied` で失敗します。

> テストモードの refresh token は **7日で失効**します。失効したら設定画面で「接続を解除」→
> 「Googleに接続」をやり直してください。恒久的に使いたい場合はアプリを「本番」に公開します
> (自分だけが使う機密スコープなら審査なしで公開できますが、警告画面は出ます)。

## 4. OAuth クライアントID を発行する

左メニュー「API とサービス」→「認証情報」→「認証情報を作成」→「OAuth クライアント ID」。

- **アプリケーションの種類: 「デスクトップ アプリ」を選んでください。**
- 名前は任意(例: `FastTask desktop`)

> **リダイレクトURIの入力欄は出てきません。これで正しいです。**
> 「デスクトップ アプリ」タイプは `http://127.0.0.1:<任意のポート>` へのリダイレクトが
> 自動的に許可される仕様で、FastTask は起動のたびに OS から空きポートを借りて使います
> ([Google: Loopback IP address flow](https://developers.google.com/identity/protocols/oauth2/native-app))。
> もし種類を「ウェブ アプリケーション」で作ってしまうと、ポートが毎回変わるため動きません。作り直してください。

発行されると **クライアントID** と **クライアントシークレット** が表示されます。両方控えます。
(デスクトップアプリのシークレットは Google の仕様上「機密ではない」扱いですが、FastTask は
念のためローカルに `600` パーミッションで保存します。)

## 5. FastTask に入力して接続する

1. FastTask の 設定(⌘,)→ **Google連携**
2. クライアントID / クライアントシークレットを貼り付けて「保存」
3. 「Googleに接続」を押す → 既定のブラウザが開く
4. アカウントを選び、権限を許可する
- 「このアプリは確認されていません」と出たら「詳細」→「(アプリ名)に移動」を押す(テストモードのため)
5. ブラウザに「連携が完了しました」と出たらタブを閉じる。設定画面に接続中のアカウントが表示されます。

## 6. 書き出し設定

| 項目 | 内容 |
| --- | --- |
| ToDoに書き出す | タスクを Google ToDo に作成。FastTask で完了にすると Google 側も完了になります |
| カレンダーに書き出す | **期日のあるタスクだけ**を、その日の終日イベントとして作成します |
| 自動書き出し | タスクの追加・更新時に自動で push(既定OFF)。失敗してもアプリの操作は止まりません |
| カレンダーID | 既定 `primary`(メインのカレンダー)。別カレンダーに入れるなら Google カレンダーの設定からIDをコピー |
| ToDoリスト | 既定 `@default`。「リスト取得」を押すとリスト名から選べます |
| 今すぐ同期 | 全タスクを手動で push |

---

## 仕様・制限

- **一方向だけです。** Google 側の変更は FastTask に戻ってきません。
- **FastTask でタスクを削除しても、Google 側は削除されません**(対応表から外れるだけ)。
Google 側で手動で消してください。
- 期日を外すと、そのタスクのカレンダー予定は削除されます。
- 前回 push した内容(タイトル・状態・期日・URL)と変わっていないタスクは API を呼びません。
- 自動書き出しは保存のたびに走りますが、前回の push が実行中なら今回はスキップします
(最新の状態は次の保存で反映されます)。

## 保存される場所

アプリデータフォルダ(macOS は `~/Library/Application Support/com.fasttask.desktop/`)。

| ファイル | 中身 |
| --- | --- |
| `google.json` | クライアントID・シークレット・リフレッシュトークン・各種設定(**0600**) |
| `google_map.json` | FastTask のタスクID ↔ Google 側ID の対応表 |

タスク本体(`data.json`)に認証情報は混ざりません。
「接続を解除」を押すと Google 側でトークンを失効させ、手元の認証情報と対応表を消します。

## うまくいかないとき

| 症状 | 原因と対処 |
| --- | --- |
| `access_denied` | OAuth同意画面の「テストユーザー」に自分のアカウントが入っていない |
| `refresh_token が返りませんでした` | 同じクライアントで過去に許可済み。設定で「接続を解除」してからやり直す |
| `invalid_client` | クライアントID/シークレットの貼り間違い、または種類が「デスクトップ アプリ」でない |
| 数日経つと同期エラー | テストモードの7日失効。再接続するか、アプリを本番公開する |
| `Tasks リストが見つかりません` | ToDoリストIDが違う。「リスト取得」で選び直す |
| 認証がタイムアウトしました | ブラウザでの許可を5分以内に完了する必要があります |
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# FastTask Docs

FastTask は macOS 向けの軽量タスク管理アプリです。ここにはアプリの設定や外部連携のセットアップ手順を置いています。

## ドキュメント

- [Google連携セットアップ](google-sync.md) — FastTask のタスクを Google カレンダー / Google ToDo へ書き出すための OAuth クライアント設定手順

## リンク

- [リポジトリ](https://github.com/riiimparm/FastTask)
- [リリース](https://github.com/riiimparm/FastTask/releases)
70 changes: 70 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# FastTask MCP サーバ

FastTask が起動時に立てるローカル HTTP API (127.0.0.1 のみ) を叩く、依存ゼロの MCP stdio ブリッジ。

必要条件: Node 18+ / FastTask アプリが起動していること。

## 仕組み

FastTask は起動時に `app_data_dir/api.json` へ実ポートと認証トークンを書き出す。

- macOS: `~/Library/Application Support/com.fasttask.desktop/api.json`
- Linux: `~/.local/share/com.fasttask.desktop/api.json`
- Windows: `%APPDATA%\com.fasttask.desktop\api.json`

MCP サーバはこのファイルを毎リクエスト読み直すので、設定は不要。環境変数
`FASTTASK_PORT` / `FASTTASK_TOKEN` / `FASTTASK_APP_DATA_DIR` で上書きできる。

## 登録

Claude Code:

```sh
claude mcp add fasttask -- node /absolute/path/to/FastTask/mcp/server.js
```

Claude Desktop (`claude_desktop_config.json`) / 手動設定:

```json
{
"mcpServers": {
"fasttask": {
"command": "node",
"args": ["/absolute/path/to/FastTask/mcp/server.js"]
}
}
}
```

## tools

| tool | 引数 |
| --- | --- |
| `list_tasks` | `status?` (`todo` \| `done`) |
| `create_task` | `title` (必須), `dueDate?`, `url?`, `tags?` |
| `update_task` | `id` (必須), `title?`, `status?`, `dueDate?`, `url?`, `tags?` |
| `delete_task` | `id` (必須) — 子タスクも一緒に削除 |

## HTTP API を直接叩く

全リクエストに `Authorization: Bearer <token>` が必須 (無いと 401)。

```sh
BASE=http://127.0.0.1:$(sed -n 's/.*"port": *\([0-9]*\).*/\1/p' ~/Library/Application\ Support/com.fasttask.desktop/api.json)
TOKEN=$(sed -n 's/.*"token": *"\([^"]*\)".*/\1/p' ~/Library/Application\ Support/com.fasttask.desktop/api.json)

curl "$BASE/tasks?status=todo" -H "Authorization: Bearer $TOKEN"
curl -X POST "$BASE/tasks" -H "Authorization: Bearer $TOKEN" -d '{"title":"資料を書く","dueDate":"2026-11-10"}'
curl -X PATCH "$BASE/tasks/<id>" -H "Authorization: Bearer $TOKEN" -d '{"status":"done"}'
curl -X DELETE "$BASE/tasks/<id>" -H "Authorization: Bearer $TOKEN"
```

## 動作確認

FastTask を起動した状態で:

```sh
sh mcp/smoke.sh
```

トークン無し → 401、作成 → 一覧に出る、を確認して後片付けする。
135 changes: 135 additions & 0 deletions mcp/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env node
// FastTask MCP stdio bridge -> ローカルHTTP API。依存なし (Node 18+ の fetch を使用)。
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { createInterface } from "node:readline";

const APP_DATA_DIR =
process.env.FASTTASK_APP_DATA_DIR ||
(process.platform === "darwin"
? join(homedir(), "Library", "Application Support", "com.fasttask.desktop")
: process.platform === "win32"
? join(process.env.APPDATA || "", "com.fasttask.desktop")
: join(homedir(), ".local", "share", "com.fasttask.desktop"));

function config() {
let port = process.env.FASTTASK_PORT;
let token = process.env.FASTTASK_TOKEN;
if (!port || !token) {
const f = JSON.parse(readFileSync(join(APP_DATA_DIR, "api.json"), "utf8"));
port ||= f.port;
token ||= f.token;
}
return { base: `http://127.0.0.1:${port}`, token };
}

async function api(method, path, body) {
const { base, token } = config();
const res = await fetch(base + path, {
method,
headers: {
Authorization: `Bearer ${token}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (!res.ok) throw new Error(`${res.status}: ${text}`);
return text;
}

const TOOLS = [
{
name: "list_tasks",
description: "FastTask のタスク一覧を取得する",
inputSchema: {
type: "object",
properties: { status: { type: "string", enum: ["todo", "done"] } },
},
run: (a) => api("GET", `/tasks${a.status ? `?status=${encodeURIComponent(a.status)}` : ""}`),
},
{
name: "create_task",
description: "FastTask にタスクを作成する",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
dueDate: { type: "string", description: "ISO 日付 (YYYY-MM-DD)" },
url: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["title"],
},
run: (a) => api("POST", "/tasks", a),
},
{
name: "update_task",
description: "タスクを部分更新する (title / status / dueDate / url / tags)",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
status: { type: "string", enum: ["todo", "done"] },
dueDate: { type: "string" },
url: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["id"],
},
run: ({ id, ...patch }) => api("PATCH", `/tasks/${encodeURIComponent(id)}`, patch),
},
{
name: "delete_task",
description: "タスクを削除する (子タスクも削除される)",
inputSchema: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
run: ({ id }) => api("DELETE", `/tasks/${encodeURIComponent(id)}`),
},
];

function send(msg) {
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
}

async function handle(req) {
switch (req.method) {
case "initialize":
return {
protocolVersion: req.params?.protocolVersion || "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "fasttask", version: "0.1.0" },
};
case "tools/list":
return { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) };
case "tools/call": {
const tool = TOOLS.find((t) => t.name === req.params?.name);
if (!tool) throw new Error(`unknown tool: ${req.params?.name}`);
const text = await tool.run(req.params.arguments || {});
return { content: [{ type: "text", text }] };
}
default:
throw new Error(`unknown method: ${req.method}`);
}
}

createInterface({ input: process.stdin }).on("line", async (line) => {
if (!line.trim()) return;
let req;
try {
req = JSON.parse(line);
} catch {
return;
}
try {
const result = await handle(req);
if (req.id !== undefined) send({ id: req.id, result });
} catch (e) {
if (req.id !== undefined) send({ id: req.id, error: { code: -32000, message: String(e.message || e) } });
}
});
Loading
Loading