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
26 changes: 25 additions & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ pnpm today --notion-board "https://www.notion.so/workspace/Tasks-0123456789abcde

WorkCue는 Notion row의 title, status, due date, priority, assignee, project, labels, estimate 같은 속성을 읽습니다. 이 preview에서는 page body 내용 전체를 읽지 않습니다.

업무 source에 실제 프로젝트 레포 맥락을 함께 붙이려면:

```bash
pnpm today \
--notion-board "https://www.notion.so/workspace/Tasks-0123456789abcdef0123456789abcdef" \
--project-path /path/to/project \
--project-remote https://github.com/example/project \
--date 2026-05-29
```

Project context는 read-only로 동작합니다. WorkCue는 git branch, 변경 중인 파일, 최근 commit 제목, 주요 manifest, docs, TODO/FIXME marker를 읽고, 연결되는 업무 항목에 짧은 evidence summary만 붙입니다. 로컬 절대경로는 brief나 sync JSON에 포함하지 않습니다.

brief를 만들기 전에 정규화된 source item만 확인하려면:

```bash
Expand Down Expand Up @@ -142,14 +154,15 @@ Top recommendation: Review PR #184: Fix payment retry race condition
- GitHub Issues/PR connector
- Jira issue connector
- Notion kanban database/data source connector preview
- 로컬 git repo용 project context analyzer
- Markdown morning brief renderer
- Markdown file output
- Obsidian daily note upsert
- MCP stdio server: `workcue_sync`, `workcue_today`, `workcue_explain`, `workcue_doctor`
- sync 결과용 로컬 SQLite cache
- 로컬 container 실행용 Dockerfile
- CLI commands: `workcue sync`, `workcue explain`, `workcue today --demo`
- CLI source options: `--obsidian-vault <path>`, `--notion-board <url-or-id>`
- CLI source options: `--obsidian-vault <path>`, `--notion-board <url-or-id>`, `--project-path <path>`, `--project-remote <url>`

## 제품 원칙

Expand Down Expand Up @@ -213,6 +226,17 @@ Notion token 값은 config에 저장하지 않고 환경변수에 둡니다.
export NOTION_TOKEN="secret_..."
```

프로젝트 레포 맥락을 config에 추가할 수도 있습니다.

```bash
pnpm --filter workcue dev init \
--output .workcue/config.yml \
--project-path /path/to/project \
--project-remote https://github.com/example/project
```

`.workcue/config.yml`에 private board URL, repository path, private remote URL이 들어 있다면 커밋하지 않는 것이 안전합니다.

config를 점검합니다.

```bash
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ pnpm today --notion-board "https://www.notion.so/workspace/Tasks-0123456789abcde

WorkCue reads Notion row properties such as title, status, due date, priority, assignee, project, labels, and estimate. It does not read page body content in this preview.

To add project repository context to any source:

```bash
pnpm today \
--notion-board "https://www.notion.so/workspace/Tasks-0123456789abcdef0123456789abcdef" \
--project-path /path/to/project \
--project-remote https://github.com/example/project \
--date 2026-05-29
```

Project context is read-only. WorkCue looks at git branch, dirty files, recent commit subjects, common manifests, docs, and TODO/FIXME markers, then attaches a short evidence summary to matching work items. Local absolute paths are not included in the brief or sync JSON.

To inspect normalized source items without generating a brief:

```bash
Expand Down Expand Up @@ -142,14 +154,15 @@ Top recommendation: Review PR #184: Fix payment retry race condition
- GitHub Issues and PR connector package
- Jira issue connector package
- Notion kanban database/data source connector preview
- Project repository context analyzer for local git repos
- Markdown morning brief renderer
- Markdown file output
- Obsidian daily note upsert
- MCP stdio server with `workcue_sync`, `workcue_today`, `workcue_explain`, and `workcue_doctor`
- Local SQLite cache for sync results
- Dockerfile for local container runs
- CLI commands: `workcue sync`, `workcue explain`, `workcue today --demo`
- CLI source options: `--obsidian-vault <path>`, `--notion-board <url-or-id>`
- CLI source options: `--obsidian-vault <path>`, `--notion-board <url-or-id>`, `--project-path <path>`, `--project-remote <url>`

## Product Principles

Expand Down Expand Up @@ -213,6 +226,17 @@ The Notion token value stays in the environment:
export NOTION_TOKEN="secret_..."
```

Add project repository context:

```bash
pnpm --filter workcue dev init \
--output .workcue/config.yml \
--project-path /path/to/project \
--project-remote https://github.com/example/project
```

Do not commit `.workcue/config.yml` when it contains private board URLs, repository paths, or private remote URLs.

Check the config:

```bash
Expand Down
50 changes: 50 additions & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
explainWorkCueItem,
renderRecommendationExplanation,
runWorkCueToday,
serializeProjectContext,
syncWorkCueSources,
writeWorkCueOutputs,
type RunWorkCueTodayOptions
Expand All @@ -33,13 +34,17 @@ program
.option("--output <path>", "Config path to write.", defaultConfigPath())
.option("--obsidian-vault <path>", "Local Obsidian vault path to enable.")
.option("--notion-board <url-or-id>", "Notion kanban database or data source URL/ID to enable.")
.option("--project-path <path>", "Local project repository path to use as recommendation context.")
.option("--project-remote <url>", "Project remote URL to match GitHub/Notion/Jira work items.")
.option("--markdown-output <path>", "Markdown output path. Supports {{date}}.")
.option("--daily-note <path>", "Daily note output path. Supports {{date}}.")
.action(
async (options: {
output: string;
obsidianVault?: string;
notionBoard?: string;
projectPath?: string;
projectRemote?: string;
markdownOutput?: string;
dailyNote?: string;
}) => {
Expand All @@ -50,6 +55,12 @@ program
if (options.notionBoard) {
initOptions.notionBoard = options.notionBoard;
}
if (options.projectPath) {
initOptions.projectPath = options.projectPath;
}
if (options.projectRemote) {
initOptions.projectRemote = options.projectRemote;
}
if (options.markdownOutput) {
initOptions.markdownOutput = options.markdownOutput;
}
Expand Down Expand Up @@ -111,6 +122,10 @@ program
lines.push("Notion: disabled");
}

lines.push(
config.projects.length > 0 ? `Project context: configured ${config.projects.length} project(s)` : "Project context: disabled"
);

lines.push(config.outputs.markdown.enabled ? "Markdown output: enabled" : "Markdown output: disabled");
lines.push(config.outputs.dailyNote.enabled ? "Daily note output: enabled" : "Daily note output: disabled");
lines.push(config.cache.sqlite.enabled ? `SQLite cache: enabled ${config.cache.sqlite.path}` : "SQLite cache: disabled");
Expand All @@ -127,6 +142,8 @@ program
.option("--obsidian-vault <path>", "Read unchecked markdown tasks from a local Obsidian vault.")
.option("--notion-board <url-or-id>", "Read cards from a Notion kanban database or data source.")
.option("--notion-token-env <name>", "Environment variable name that stores the Notion integration token.", "NOTION_TOKEN")
.option("--project-path <path>", "Local project repository path to use as recommendation context.")
.option("--project-remote <url>", "Project remote URL to match GitHub/Notion/Jira work items.")
.option("--assignee <handle>", "Assignee handle to attach to local tasks.", "you")
.option("--date <date>", "Sync date in YYYY-MM-DD format.", todayDate())
.option("--json", "Print a JSON payload.")
Expand All @@ -144,6 +161,8 @@ program
notionTokenEnv?: string;
obsidianVault?: string;
output?: string;
projectPath?: string;
projectRemote?: string;
}) => {
const result = await syncWorkCueSources(buildRunOptions(options));
const payload = buildSyncPayload(result);
Expand Down Expand Up @@ -179,6 +198,8 @@ program
.option("--obsidian-vault <path>", "Read unchecked markdown tasks from a local Obsidian vault.")
.option("--notion-board <url-or-id>", "Read cards from a Notion kanban database or data source.")
.option("--notion-token-env <name>", "Environment variable name that stores the Notion integration token.", "NOTION_TOKEN")
.option("--project-path <path>", "Local project repository path to use as recommendation context.")
.option("--project-remote <url>", "Project remote URL to match GitHub/Notion/Jira work items.")
.option("--output <path>", "Write the generated brief to a markdown file.")
.option("--daily-note <path>", "Upsert the generated brief into a markdown daily note.")
.option("--assignee <handle>", "Assignee handle to attach to local tasks.", "you")
Expand All @@ -193,6 +214,8 @@ program
obsidianVault?: string;
output?: string;
dailyNote?: string;
projectPath?: string;
projectRemote?: string;
assignee: string;
date: string;
top: number;
Expand Down Expand Up @@ -226,6 +249,8 @@ program
.option("--obsidian-vault <path>", "Read unchecked markdown tasks from a local Obsidian vault.")
.option("--notion-board <url-or-id>", "Read cards from a Notion kanban database or data source.")
.option("--notion-token-env <name>", "Environment variable name that stores the Notion integration token.", "NOTION_TOKEN")
.option("--project-path <path>", "Local project repository path to use as recommendation context.")
.option("--project-remote <url>", "Project remote URL to match GitHub/Notion/Jira work items.")
.option("--assignee <handle>", "Assignee handle to attach to local tasks.", "you")
.option("--date <date>", "Brief date in YYYY-MM-DD format.", todayDate())
.option("--json", "Print a JSON explanation payload.")
Expand All @@ -241,6 +266,8 @@ program
notionBoard?: string;
notionTokenEnv?: string;
obsidianVault?: string;
projectPath?: string;
projectRemote?: string;
}
) => {
const recommendation = await explainWorkCueItem({
Expand Down Expand Up @@ -285,6 +312,8 @@ function buildRunOptions(options: {
notionBoard?: string;
notionTokenEnv?: string;
obsidianVault?: string;
projectPath?: string;
projectRemote?: string;
top?: number;
}): RunWorkCueTodayOptions {
const runOptions: RunWorkCueTodayOptions = {
Expand All @@ -306,6 +335,12 @@ function buildRunOptions(options: {
if (options.notionTokenEnv) {
runOptions.notionTokenEnv = options.notionTokenEnv;
}
if (options.projectPath) {
runOptions.projectPath = options.projectPath;
}
if (options.projectRemote) {
runOptions.projectRemote = options.projectRemote;
}
if (options.top) {
runOptions.top = options.top;
}
Expand All @@ -317,6 +352,7 @@ function buildSyncPayload(result: Awaited<ReturnType<typeof syncWorkCueSources>>
syncedAt: result.syncedAt,
itemCount: result.items.length,
sourceCounts: result.sourceCounts,
projectContexts: result.projectContexts.map(serializeProjectContext),
items: result.items.map(serializeWorkItem)
};
}
Expand Down Expand Up @@ -400,6 +436,20 @@ function serializeWorkItem(item: Awaited<ReturnType<typeof syncWorkCueSources>>[
if (item.estimateMinutes) {
payload.estimateMinutes = item.estimateMinutes;
}
if (item.projectContexts && item.projectContexts.length > 0) {
payload.projectContexts = item.projectContexts.map((context) => ({
projectId: context.projectId,
...(context.projectName ? { projectName: context.projectName } : {}),
...(context.repoName ? { repoName: context.repoName } : {}),
...(context.currentBranch ? { currentBranch: context.currentBranch } : {}),
...(typeof context.isDirty === "boolean" ? { isDirty: context.isDirty } : {}),
signals: context.signals,
matchedTerms: context.matchedTerms.slice(0, 8),
matchedFiles: context.matchedFiles.slice(0, 8),
...(typeof context.changedFileCount === "number" ? { changedFileCount: context.changedFileCount } : {}),
...(typeof context.todoCount === "number" ? { todoCount: context.todoCount } : {})
}));
}

return payload;
}
Expand Down
27 changes: 27 additions & 0 deletions apps/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
explainWorkCueItem,
renderRecommendationExplanation,
runWorkCueToday,
serializeProjectContext,
syncWorkCueSources,
writeWorkCueOutputs,
type RunWorkCueTodayOptions
Expand All @@ -19,6 +20,8 @@ const SourceToolArgsSchema = z.object({
notionBoard: z.string().optional().describe("Notion kanban database or data source URL/ID to read cards from."),
notionTokenEnv: z.string().optional().describe("Environment variable name that stores the Notion integration token."),
obsidianVault: z.string().optional().describe("Local Obsidian vault path to read unchecked markdown tasks from."),
projectPath: z.string().optional().describe("Local project repository path to use as recommendation context."),
projectRemote: z.string().optional().describe("Project remote URL to match GitHub/Notion/Jira work items."),
top: z.number().int().positive().optional().describe("Number of focus items to return.")
});

Expand Down Expand Up @@ -132,6 +135,7 @@ export async function runTodayTool(args: TodayToolArgs): Promise<string> {
"",
`- Items read: ${result.items.length}`,
`- Source counts: ${formatSourceCounts(result.sourceCounts)}`,
`- Project contexts: ${result.projectContexts.length}`,
`- Outputs written: ${outputsWritten}`
].join("\n");
}
Expand Down Expand Up @@ -206,6 +210,8 @@ function buildRunOptions(
notionBoard?: string | undefined;
notionTokenEnv?: string | undefined;
obsidianVault?: string | undefined;
projectPath?: string | undefined;
projectRemote?: string | undefined;
top?: number | undefined;
},
date: string
Expand All @@ -226,6 +232,12 @@ function buildRunOptions(
if (args.notionTokenEnv) {
runOptions.notionTokenEnv = args.notionTokenEnv;
}
if (args.projectPath) {
runOptions.projectPath = args.projectPath;
}
if (args.projectRemote) {
runOptions.projectRemote = args.projectRemote;
}
if (args.top) {
runOptions.top = args.top;
}
Expand All @@ -240,6 +252,7 @@ function buildSyncPayload(result: Awaited<ReturnType<typeof syncWorkCueSources>>
syncedAt: result.syncedAt,
itemCount: result.items.length,
sourceCounts: result.sourceCounts,
projectContexts: result.projectContexts.map(serializeProjectContext),
items: result.items.map(serializeWorkItem)
};
}
Expand Down Expand Up @@ -279,6 +292,20 @@ function serializeWorkItem(item: Awaited<ReturnType<typeof syncWorkCueSources>>[
if (item.estimateMinutes) {
payload.estimateMinutes = item.estimateMinutes;
}
if (item.projectContexts && item.projectContexts.length > 0) {
payload.projectContexts = item.projectContexts.map((context) => ({
projectId: context.projectId,
...(context.projectName ? { projectName: context.projectName } : {}),
...(context.repoName ? { repoName: context.repoName } : {}),
...(context.currentBranch ? { currentBranch: context.currentBranch } : {}),
...(typeof context.isDirty === "boolean" ? { isDirty: context.isDirty } : {}),
signals: context.signals,
matchedTerms: context.matchedTerms.slice(0, 8),
matchedFiles: context.matchedFiles.slice(0, 8),
...(typeof context.changedFileCount === "number" ? { changedFileCount: context.changedFileCount } : {}),
...(typeof context.todoCount === "number" ? { todoCount: context.todoCount } : {})
}));
}

return payload;
}
Expand Down
14 changes: 13 additions & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ One-off Notion board:
}
```

Notion board with local project context:

```json
{
"notionBoard": "https://www.notion.so/workspace/Tasks-0123456789abcdef0123456789abcdef",
"notionTokenEnv": "NOTION_TOKEN",
"projectPath": "/path/to/project",
"projectRemote": "https://github.com/example/project",
"date": "2026-05-29"
}
```

Explain one item:

```json
Expand All @@ -84,4 +96,4 @@ Explain one item:

## Data Exposure

`workcue_sync` returns normalized item summaries and excludes connector `raw` payloads. Avoid sending generated briefs or sync JSON to shared channels if your source tools contain sensitive work.
`workcue_sync` returns normalized item summaries and excludes connector `raw` payloads. Project context summaries do not include local absolute paths, but they can include repository names, branch names, relative file names, and recent commit subjects. Avoid sending generated briefs or sync JSON to shared channels if your source tools contain sensitive work.
14 changes: 14 additions & 0 deletions docs/recipes/notion-kanban.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ pnpm doctor --config .workcue/config.yml
pnpm today --config .workcue/config.yml --date 2026-05-29
```

## Add Repository Context

If the Notion board tracks work for a specific repository, add project context so WorkCue can consider the real codebase state:

```bash
pnpm today \
--notion-board "https://www.notion.so/workspace/Tasks-0123456789abcdef0123456789abcdef" \
--project-path /path/to/project \
--project-remote https://github.com/example/project \
--date 2026-05-29
```

See [Project Context](./project-context.md) for the reusable config format.

## Property Mapping

If your board uses different property names, edit `.workcue/config.yml`:
Expand Down
Loading
Loading