diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 1a4682e..490457b 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,40 @@ # Astra Patch Notes +## v2.2.55 (2026-05-21) +### ๐Ÿ“… /meet โ€” ํšŒ์˜๋ก ์•ก์…˜ ์•„์ดํ…œ โ†’ Google ์บ˜๋ฆฐ๋” ์ž๋™ ๋“ฑ๋ก (Phase 1) +- `/meet`์ด ํšŒ์˜๋ก ํ•ฉ์„ฑยท์ €์žฅ ํ›„, **์•ก์…˜ ์•„์ดํ…œ ํ‘œ๋ฅผ ํŒŒ์‹ฑํ•ด task๋ณ„ ์ข…์ผ ์ผ์ •์œผ๋กœ Google Calendar์— ์ž๋™ ๋“ฑ๋ก**ํ•œ๋‹ค. +- ๋‚ ์งœ ๊ทœ์น™(์‚ฌ์šฉ์ž ์ •์˜): ๋ช…์‹œ ๋‚ ์งœ(`YYYY-MM-DD`/`YYYY๋…„ M์›” D์ผ`)โ†’๊ทธ๋Œ€๋กœ / "์ฐจ์ฃผยท๋‹ค์Œ ์ฃผ"โ†’ํšŒ์˜์ผ +6์ผ / "์ฆ‰์‹œยท๋‹น์ผ"โ†’๋“ฑ๋ก์ผ / ๋ณ€ํ™˜ ๋ถˆ๊ฐ€ยท๋นˆ ๊ฐ’โ†’๋“ฑ๋ก์ผ +์˜์—…์ผ 5์ผ(ํ† ยท์ผ ์ œ์™ธ, ๊ณตํœด์ผ ๋ฌด์‹œ) + ์ œ๋ชฉ์— **"(๋ฏธํ™•์ •)"** ๊ผฌ๋ฆฌํ‘œ. +- ์บ˜๋ฆฐ๋” ์ด๋ฒคํŠธ ์„ค๋ช…์— ํšŒ์˜ ์ œ๋ชฉยท๋‹ด๋‹นยท์›๋ž˜ ๊ธฐํ•œ ํ‘œ๊ธฐ๋ฅผ ๊ธฐ๋ก. Google Calendar OAuth(์“ฐ๊ธฐ)๊ฐ€ ์—ฐ๊ฒฐ๋ผ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ๋ฏธ์—ฐ๊ฒฐ ์‹œ ํšŒ์˜๋ก ์ €์žฅ๋งŒ ํ•˜๊ณ  ์•ˆ๋‚ดํ•œ๋‹ค. +- `handleSlashCommand`์— `ExtensionContext` ๋ฐฐ์„  ์ถ”๊ฐ€(`/meet`๋งŒ ์‚ฌ์šฉ). +- **์‹ ๊ทœ ํŒจํ‚ค์ง•:** `astra-2.2.55.vsix`. + +--- + + + +## v2.2.54 (2026-05-21) +### ๐Ÿ”ง ํ•œยท์˜ ํ† ํฐ ๊นจ์ง ์ถ”๊ฐ€ ๊ฐœ์„  โ€” ์ƒ˜ํ”Œ๋ง ์กฐ์ • + ๊ต์ • ํŒจ์Šค +- **์ƒ˜ํ”Œ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •.** ์Šฌ๋ž˜์‹œ ํ•ฉ์„ฑ LLM ํ˜ธ์ถœ์— `top_p`(0.85)ยท`top_k`(20)ยท`repeat_penalty`(1.1) ์ถ”๊ฐ€ โ€” ๊นจ์ง„ ์ €ํ™•๋ฅ  ํ† ํฐ("ํ•ตess" ๋“ฑ)์˜ ์ƒ˜ํ”Œ๋ง ์ž์ฒด๋ฅผ ์–ต์ œ. +- **์กฐ๊ฑด๋ถ€ ๊ต์ • ํŒจ์Šค.** ํ•ฉ์„ฑ ๊ฒฐ๊ณผ์— ํ•œยท์˜ ๊นจ์ง(`ํ•œ๊ธ€+์˜๋ฌธ ์†Œ๋ฌธ์ž ์กฐ๊ฐ`)์ด ๊ฐ์ง€๋˜๋ฉด LLM ๊ต์ • ํŒจ์Šค๋ฅผ 1ํšŒ ๋Œ๋ ค ๊นจ์ง„ ํ‘œ๊ธฐ๋งŒ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ ๊ต์ •. ๊นจ์ง์ด ์—†์œผ๋ฉด ์ถ”๊ฐ€ ํ˜ธ์ถœ ์—†์Œ. ๊ต์ • ๊ฒฐ๊ณผ๊ฐ€ ์›๋ณธ๋ณด๋‹ค ๋น„์ •์ƒ์ ์œผ๋กœ ์งง์œผ๋ฉด(์ž˜๋ผ๋จน์Œ) ์›๋ณธ์„ ์œ ์ง€ํ•˜๋Š” ์•ˆ์ „์žฅ์น˜ ํฌํ•จ. +- ์ ์šฉ ๋ฒ”์œ„: `/benchmark`ยท`/youtube`ยท`/wikify`ยท`/meet` ๋ชจ๋“  ์Šฌ๋ž˜์‹œ ํ•ฉ์„ฑ. +- **์‹ ๊ทœ ํŒจํ‚ค์ง•:** `astra-2.2.54.vsix`. + +--- + + + +## v2.2.53 (2026-05-20) +### ๐Ÿ“ ์‹ ๊ทœ /meet โ€” ํšŒ์˜ ๋…น์ทจ txt โ†’ ์‚ฌ์‹ค ๊ธฐ๋ฐ˜ ๊ตฌ์กฐํ™” ํšŒ์˜๋ก +- **์‹ ๊ทœ ์Šฌ๋ž˜์‹œ ๋ช…๋ น `/meet [๋ฉ”ํƒ€๋ฐ์ดํ„ฐ]`.** ๋กœ์ปฌ ํšŒ์˜ ๋…น์ทจ ํ…์ŠคํŠธ ํŒŒ์ผ์„ ASTRA๊ฐ€ ์ง์ ‘ ์ฝ์–ด(๋กœ์ปฌ ํŒŒ์ผ์ด๋ผ Bridge ๋ถˆํ•„์š”), ์‚ฌ์‹ค ๊ธฐ๋ฐ˜ ๊ตฌ์กฐํ™” ํšŒ์˜๋ก(Actionable Minutes)์œผ๋กœ LLM ํ•ฉ์„ฑยท์ €์žฅํ•œ๋‹ค. +- ์ฒ˜๋ฆฌ ๊ทœ์น™: Deconstruction(์žก๋‹ด ์ œ๊ฑฐ) โ†’ Classification(Fact/Discussion/Decision/Risk/Action) โ†’ Decision Logic โ†’ Structuring. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ฐธ์„์žยท๋‚ ์งœ)๊ฐ€ ๋…น์ทจ๋ก๊ณผ ์ถฉ๋Œํ•˜๋ฉด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์šฐ์„ . +- ์ถœ๋ ฅ ๊ตฌ์กฐ: ์š”์•ฝ ๋ณด๊ณ  / ์ฃผ์š” ๋…ผ์˜ ์‚ฌํ•ญ / ๋ฆฌ์Šคํฌยท์ด์Šˆ / ๊ฒฐ์ • ์‚ฌํ•ญ / ์˜คํ”ˆ ์ด์Šˆ / ์•ก์…˜ ์•„์ดํ…œ(๋‹ด๋‹นยท์ž‘์—…ยท๊ธฐํ•œ ํ‘œ). +- ๊ฒฝ๋กœ์— ๊ณต๋ฐฑ์ด ์žˆ์œผ๋ฉด ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์Œ€ ์ˆ˜ ์žˆ์Œ. ๊ฒฐ๊ณผ๋ฌผ์€ `.md`๋กœ `WIKI_RAW_PATH`(`E:\Wiki\2nd\00_Raw`)์— ์ €์žฅ. +- **์‹ ๊ทœ ํŒจํ‚ค์ง•:** `astra-2.2.53.vsix`. + +--- + + + ## v2.2.52 (2026-05-20) ### ๐Ÿ“ฆ ์žฌํŒจํ‚ค์ง• (v2.2.51 ๋™์ผ ๋‚ด์šฉ) - ๊ธฐ๋Šฅ ๋ณ€๊ฒฝ ์—†์Œ โ€” v2.2.51 ์ž‘์—… ํŠธ๋ฆฌ๋ฅผ ๋ฒ„์ „๋งŒ ์˜ฌ๋ ค ์žฌํŒจํ‚ค์ง•. ๋ฒ„์ „ ์ •ํ•ฉ์„ฑ ์ •๋ฆฌ๋ฅผ ์œ„ํ•ด `package-lock.json` ๋ฒ„์ „๋„ ํ•จ๊ป˜ 2.2.52๋กœ ๋™๊ธฐํ™”. diff --git a/package.json b/package.json index 54b5cc7..cda6d43 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.52", + "version": "2.2.55", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/features/datacollect/slashRouter.ts b/src/features/datacollect/slashRouter.ts index 3f20e07..49bd82e 100644 --- a/src/features/datacollect/slashRouter.ts +++ b/src/features/datacollect/slashRouter.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; +import { promises as fsp } from 'fs'; import { logInfo } from '../../utils'; import { bridgeFetch, getBridgeBaseUrl } from './bridgeClient'; +import { createCalendarEvent, readCalendarConfig } from '../calendar'; /** * Datacollect "๋ผ๋””์˜ค" slash ๋ช…๋ น ๋ผ์šฐํ„ฐ. @@ -16,7 +18,7 @@ import { bridgeFetch, getBridgeBaseUrl } from './bridgeClient'; * ๋ช…๋ น์ด ์ฒ˜๋ฆฌ๋˜๋ฉด true ๋ฐ˜ํ™˜ โ†’ chatHandlers๊ฐ€ ์ผ๋ฐ˜ LLM ํ๋ฆ„์œผ๋กœ ์•ˆ ๋‚ด๋ ค๊ฐ€๊ฒŒ. */ -const COMMANDS = ['/research', '/benchmark', '/youtube', '/blog', '/wikify'] as const; +const COMMANDS = ['/research', '/benchmark', '/youtube', '/blog', '/wikify', '/meet'] as const; type SlashCommand = typeof COMMANDS[number]; export function isSlashCommand(input: string): boolean { @@ -48,6 +50,7 @@ function chunk(view: Webview | undefined, value: string) { export async function handleSlashCommand( input: string, view: Webview | undefined, + context?: vscode.ExtensionContext, ): Promise { const trimmed = input.trim(); const spaceIdx = trimmed.indexOf(' '); @@ -73,6 +76,7 @@ export async function handleSlashCommand( case '/youtube': return await runYoutube(arg, view); case '/blog': return await runBlog(arg, view); case '/wikify': return await runWikify(arg, view); + case '/meet': return await runMeet(arg, view, context); } return true; } catch (e: any) { @@ -436,6 +440,11 @@ async function callLmSynthesis(prompt: string, systemPrompt?: string): Promise { + try { + const res = await bridgeFetch('/api/lm', { + method: 'POST', + body: JSON.stringify({ + url: `${lmUrl}/v1/chat/completions`, + payload: { + model, + messages: [ + { role: 'system', content: '๋‹น์‹ ์€ ํ•œ๊ตญ์–ด ๊ต์ •๊ธฐ์ž…๋‹ˆ๋‹ค. ์ž…๋ ฅ ํ…์ŠคํŠธ์—์„œ ํ•œ๊ธ€๊ณผ ์˜๋ฌธ ์•ŒํŒŒ๋ฒณ์ด ํ•œ ๋‹จ์–ด๋กœ ์ž˜๋ชป ํ•ฉ์ณ์ง„ ๊นจ์ง„ ํ‘œ๊ธฐ(์˜ˆ: "ํ•ตess"โ†’"ํ•ต์‹ฌ", "๊ฒฐently"โ†’"๊ฒฐ๊ตญ")๋งŒ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ ๊ต์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์™ธ ๋‚ด์šฉยท๋ฌธ์žฅยท๋งˆํฌ๋‹ค์šด ๊ตฌ์กฐยทํ‘œยท์ •์ƒ์ ์ธ ์˜๋ฌธ ์šฉ์–ด(API, JSON ๋“ฑ)๋Š” ํ•œ ๊ธ€์ž๋„ ๋ฐ”๊พธ์ง€ ์•Š์œผ๋ฉฐ, ๊ต์ •๋œ ์ „์ฒด ํ…์ŠคํŠธ๋งŒ ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค(์„ค๋ช…ยท์ฃผ์„ ๊ธˆ์ง€).' }, + { role: 'user', content: text }, + ], + temperature: 0, + top_p: 0.7, + top_k: 20, + }, + }), + }, { timeoutMs: 120_000 }); + const fixed = String( + res?.choices?.[0]?.message?.content + ?? res?.choices?.[0]?.text + ?? res?.answer ?? res?.response ?? '', + ).replace(/\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i, '').trim(); + // ์•ˆ์ „์žฅ์น˜ โ€” ๊ต์ • ๊ฒฐ๊ณผ๊ฐ€ ์›๋ณธ์˜ 70% ๋ฏธ๋งŒ์ด๋ฉด LLM์ด ๋‚ด์šฉ์„ ์ž˜๋ผ๋จน์€ ๊ฒƒ์ด๋ฏ€๋กœ ์›๋ณธ ์‚ฌ์šฉ. + if (!fixed || fixed.length < text.length * 0.7) return text; + return fixed; + } catch { + return text; // ๊ต์ • ์‹คํŒจ๋Š” ๋น„์น˜๋ช…์  โ€” ์›๋ณธ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜. + } } async function runBenchmark(arg: string, view: Webview | undefined): Promise { @@ -1083,3 +1134,286 @@ async function runWikify(arg: string, view: Webview | undefined): Promise { + // ๊ฒฝ๋กœ ํŒŒ์‹ฑ โ€” ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ๋ฉด ๊ณต๋ฐฑ ํฌํ•จ ๊ฒฝ๋กœ ํ—ˆ์šฉ, ์•„๋‹ˆ๋ฉด ์ฒซ ๊ณต๋ฐฑ ์ „๊นŒ์ง€๊ฐ€ ๊ฒฝ๋กœ. + const trimmed = arg.trim(); + let filePath = ''; + let metadata = ''; + if (trimmed.startsWith('"')) { + const end = trimmed.indexOf('"', 1); + if (end > 0) { + filePath = trimmed.slice(1, end); + metadata = trimmed.slice(end + 1).trim(); + } + } + if (!filePath) { + const sp = trimmed.indexOf(' '); + if (sp === -1) { + filePath = trimmed; + } else { + filePath = trimmed.slice(0, sp); + metadata = trimmed.slice(sp + 1).trim(); + } + } + if (!filePath) { + chunk(view, `์‚ฌ์šฉ๋ฒ•: \`/meet [์ฐธ์„์žยท๋‚ ์งœ ๋“ฑ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ]\`\n์˜ˆ: \`/meet c:\\doc\\0101.txt\`\n๊ฒฝ๋กœ์— ๊ณต๋ฐฑ์ด ์žˆ์œผ๋ฉด ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ์„ธ์š”: \`/meet "c:\\my docs\\0101.txt"\`\n`); + return true; + } + + chunk(view, `๐Ÿ“ **ํšŒ์˜๋ก ์ž‘์„ฑ**: ${filePath}\n\nโณ ๋…น์ทจ ํŒŒ์ผ ์ฝ๋Š” ์ค‘โ€ฆ`); + + // 1) ๋กœ์ปฌ txt ํŒŒ์ผ ์ฝ๊ธฐ โ€” ASTRA๊ฐ€ ์ง์ ‘ (๋กœ์ปฌ ํŒŒ์ผ์ด๋ผ Bridge ๋ถˆํ•„์š”). + let transcript: string; + try { + transcript = await fsp.readFile(filePath, 'utf-8'); + } catch (e: any) { + chunk(view, `\n\nโŒ ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${e?.message || String(e)}\n๊ฒฝ๋กœ๊ฐ€ ์ •ํ™•ํ•œ์ง€ ํ™•์ธํ•˜์„ธ์š”.\n`); + return true; + } + if (!transcript || transcript.trim().length < 20) { + chunk(view, `\n\nโš ๏ธ ํŒŒ์ผ ๋‚ด์šฉ์ด ๊ฑฐ์˜ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.\n`); + return true; + } + // LLM ์ž…๋ ฅ ํญ์ฃผ ๋ฐฉ์ง€ โ€” 60000์ž ์ƒํ•œ. + const MAX = 60000; + const truncated = transcript.length > MAX; + if (truncated) transcript = transcript.slice(0, MAX); + chunk(view, `\nโœ… ํŒŒ์ผ ์ฝ๊ธฐ ์™„๋ฃŒ (${transcript.length.toLocaleString()}์ž${truncated ? ', ์ƒํ•œ ์ดˆ๊ณผ๋กœ ์ผ๋ถ€ ์ž˜๋ฆผ' : ''})\n\n`); + + // 2) LLM ํšŒ์˜๋ก ํ•ฉ์„ฑ. + const cfg = vscode.workspace.getConfiguration('g1nation'); + const model = (cfg.get('defaultModel', '') || 'gemma4:e2b').trim(); + const meetSystem = '๋‹น์‹ ์€ ํšŒ์˜๋ก ์ž‘์„ฑ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ๋…น์ทจ๋ก๊ณผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ ๊ทผ๊ฑฐ๋กœ ์‚ฌ์‹ค ๊ธฐ๋ฐ˜์˜ ๊ตฌ์กฐํ™”๋œ ํšŒ์˜๋ก์„ ์ž‘์„ฑํ•˜๋ฉฐ, ์™ธ๋ถ€ ์ง€์‹์ด๋‚˜ ์ถ”์ธก์„ ์ ˆ๋Œ€ ์„ž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ถœ๋ ฅ์€ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.'; + chunk(view, `๐Ÿงช **ํšŒ์˜๋ก ํ•ฉ์„ฑ** (๋ชจ๋ธ \`${model}\`)\n๋ชจ๋ธยทํ•˜๋“œ์›จ์–ด์— ๋”ฐ๋ผ ์ˆ˜ ๋ถ„ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹คโ€ฆ`); + let report: string; + try { + const t0 = Date.now(); + report = await callLmSynthesis(buildMeetPrompt(transcript, metadata), meetSystem); + if (!report) throw new Error('LLM ์‘๋‹ต์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.'); + chunk(view, ` โœ“ (${Math.round((Date.now() - t0) / 1000)}s)\n\n`); + } catch (e: any) { + chunk(view, `\n\nโš ๏ธ ํšŒ์˜๋ก ํ•ฉ์„ฑ ์‹คํŒจ: ${e?.message || String(e)}\n(LM ์„œ๋ฒ„๊ฐ€ ๋–  ์žˆ๋Š”์ง€, \`g1nation.ollamaUrl\` / \`defaultModel\` ์„ค์ •์„ ํ™•์ธํ•˜์„ธ์š”.)\n\n`); + return true; + } + chunk(view, report + '\n\n'); + + // 3) ์ €์žฅ โ€” /api/wiki/save (datacollectSavePath > WIKI_RAW_PATH). + // WIKI_RAW_PATH๊ฐ€ E:\Wiki\2nd\00_Raw ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋ฏ€๋กœ ๊ฒฐ๊ณผ๋ฌผ์ด ๊ทธ๊ณณ์— .md๋กœ ์ €์žฅ๋œ๋‹ค. + try { + const today = new Date().toISOString().slice(0, 10); + const baseName = filePath.replace(/^.*[\\/]/, '').replace(/\.[^.]+$/, '') || 'meeting'; + const title = `ํšŒ์˜๋ก ${baseName} ${today}`; + const savePath = (cfg.get('datacollectSavePath', '') || '').trim(); + const body: Record = { title, content: report }; + if (savePath) body.saveDir = savePath; + const saved = await bridgeFetch<{ success: boolean; path?: string }>( + '/api/wiki/save', + { method: 'POST', body: JSON.stringify(body) }, + { timeoutMs: 30_000 }, + ); + chunk(view, `๐Ÿ’พ **ํšŒ์˜๋ก ์ €์žฅ ์™„๋ฃŒ**: \`${saved?.path || '(๊ฒฝ๋กœ ๋ฏธํ™•์ธ)'}\`\n`); + } catch (e: any) { + chunk(view, `โš ๏ธ ํšŒ์˜๋ก ์ €์žฅ ์‹คํŒจ: ${e?.message || String(e)}\n`); + } + + // 4) ์บ˜๋ฆฐ๋” ์ž๋™ ๋“ฑ๋ก โ€” ์•ก์…˜ ์•„์ดํ…œ์„ task๋ณ„ ์ข…์ผ ์ผ์ •์œผ๋กœ Google Calendar์— ๋“ฑ๋ก. + if (context) { + try { + const calCfg = readCalendarConfig(context); + if (!calCfg.refreshToken) { + chunk(view, `\nโ„น๏ธ ์บ˜๋ฆฐ๋” ์ž๋™ ๋“ฑ๋ก์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค โ€” Google Calendar OAuth(์“ฐ๊ธฐ)๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. (Astra Settings โ†’ Google ์„น์…˜์—์„œ ์—ฐ๊ฒฐ)\n`); + } else { + const tasks = parseActionItems(report); + if (tasks.length === 0) { + chunk(view, `\nโ„น๏ธ ์•ก์…˜ ์•„์ดํ…œ์ด ์—†์–ด ์บ˜๋ฆฐ๋”์— ๋“ฑ๋กํ•  ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค.\n`); + } else { + const today = new Date(); + const meetingDate = extractMeetingDate(report, today); + const titleMatch = report.match(/^#\s+(.+)$/m); + const meetTitle = titleMatch + ? titleMatch[1].replace(/^\[ํšŒ์˜ ์ œ๋ชฉ\]\s*/, '').trim() + : 'ํšŒ์˜'; + chunk(view, `\n๐Ÿ“… **์บ˜๋ฆฐ๋” ๋“ฑ๋ก**: ์•ก์…˜ ์•„์ดํ…œ ${tasks.length}๊ฑดโ€ฆ\n`); + let ok = 0; + let tentativeCount = 0; + for (const task of tasks) { + const { date, tentative } = resolveTaskDate(task.due, meetingDate, today); + if (tentative) tentativeCount++; + const evTitle = tentative ? `${task.work} (๋ฏธํ™•์ •)` : task.work; + const result = await createCalendarEvent(context, { + title: evTitle, + start: date, + allDay: true, + description: `ํšŒ์˜๋ก: ${meetTitle}\n๋‹ด๋‹น: ${task.owner}\n๊ธฐํ•œ ํ‘œ๊ธฐ: ${task.due || '(์—†์Œ)'}\nAstra /meet ์ž๋™ ๋“ฑ๋ก`, + }); + if (result.ok) { + ok++; + chunk(view, ` ยท ${date} โ€” ${evTitle}\n`); + } else { + chunk(view, ` ยท โš ๏ธ ๋“ฑ๋ก ์‹คํŒจ (${task.work}): ${result.error}\n`); + } + } + chunk(view, `โœ… ์บ˜๋ฆฐ๋” ${ok}/${tasks.length}๊ฑด ๋“ฑ๋ก ์™„๋ฃŒ${tentativeCount > 0 ? ` ยท ๋ฏธํ™•์ • ${tentativeCount}๊ฑด` : ''}\n`); + } + } + } catch (e: any) { + chunk(view, `\nโš ๏ธ ์บ˜๋ฆฐ๋” ๋“ฑ๋ก ์ค‘ ์˜ค๋ฅ˜: ${e?.message || String(e)}\n`); + } + } + return true; +} + +// โ”€โ”€โ”€ /meet ์บ˜๋ฆฐ๋” ๋“ฑ๋ก ํ—ฌํผ โ”€โ”€โ”€ + +/** ํ† ยท์ผ์„ ์ œ์™ธํ•˜๊ณ  ์˜์—…์ผ n์ผ์„ ๋”ํ•œ ๋‚ ์งœ (๊ณตํœด์ผ์€ ๊ณ ๋ คํ•˜์ง€ ์•Š์Œ). */ +function addBusinessDays(base: Date, n: number): Date { + const r = new Date(base); + let added = 0; + while (added < n) { + r.setDate(r.getDate() + 1); + const day = r.getDay(); + if (day !== 0 && day !== 6) added++; + } + return r; +} + +/** Date โ†’ 'YYYY-MM-DD' (๋กœ์ปฌ ๊ธฐ์ค€). */ +function toYmd(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +/** ํšŒ์˜๋ก ๋ณธ๋ฌธ์˜ "**๋‚ ์งœ**: 2026๋…„ 05์›” 08์ผ"์—์„œ ํšŒ์˜ ๋‚ ์งœ ์ถ”์ถœ. ์—†์œผ๋ฉด fallback. */ +function extractMeetingDate(report: string, fallback: Date): Date { + const m = report.match(/๋‚ ์งœ\*{0,2}\s*[:๏ผš]\s*\*{0,2}\s*(\d{4})\s*๋…„\s*(\d{1,2})\s*์›”\s*(\d{1,2})\s*์ผ/); + if (m) { + const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); + if (!isNaN(d.getTime())) return d; + } + return fallback; +} + +/** + * ์•ก์…˜ ์•„์ดํ…œ '๊ธฐํ•œ' ํ…์ŠคํŠธ โ†’ ์บ˜๋ฆฐ๋” ๋“ฑ๋ก ๋‚ ์งœ. ์‚ฌ์šฉ์ž ์ •์˜ ๊ทœ์น™: + * - ๋ช…์‹œ ๋‚ ์งœ(YYYY-MM-DD / YYYY๋…„ M์›” D์ผ) โ†’ ๊ทธ ๋‚ ์งœ + * - "์ฐจ์ฃผ / ๋‹ค์Œ ์ฃผ / ๋‚ด์ฃผ" โ†’ ํšŒ์˜์ผ +6์ผ + * - "์ฆ‰์‹œ / ๋‹น์ผ / ๊ธˆ์ผ / ๋ฐ”๋กœ / ์˜ค๋Š˜" โ†’ ๋“ฑ๋ก์ผ(์˜ค๋Š˜) + * - ๋ณ€ํ™˜ ๋ถˆ๊ฐ€ / ๋นˆ ๊ฐ’ โ†’ ๋“ฑ๋ก์ผ +์˜์—…์ผ 5์ผ, tentative=true (์ œ๋ชฉ์— "(๋ฏธํ™•์ •)") + */ +function resolveTaskDate(due: string, meetingDate: Date, today: Date): { date: string; tentative: boolean } { + const t = (due || '').trim(); + const iso = t.match(/(\d{4})-(\d{1,2})-(\d{1,2})/); + if (iso) { + return { date: `${iso[1]}-${iso[2].padStart(2, '0')}-${iso[3].padStart(2, '0')}`, tentative: false }; + } + const kor = t.match(/(\d{4})\s*๋…„\s*(\d{1,2})\s*์›”\s*(\d{1,2})\s*์ผ/); + if (kor) { + return { date: toYmd(new Date(Number(kor[1]), Number(kor[2]) - 1, Number(kor[3]))), tentative: false }; + } + if (/์ฐจ์ฃผ|๋‹ค์Œ\s*์ฃผ|๋‚ด์ฃผ/.test(t)) { + const d = new Date(meetingDate); + d.setDate(d.getDate() + 6); + return { date: toYmd(d), tentative: false }; + } + if (/์ฆ‰์‹œ|๋‹น์ผ|๊ธˆ์ผ|๋ฐ”๋กœ|์˜ค๋Š˜/.test(t)) { + return { date: toYmd(today), tentative: false }; + } + // ๋ณ€ํ™˜ ๋ถˆ๊ฐ€ โ€” ๋“ฑ๋ก์ผ + ์˜์—…์ผ 5์ผ, "(๋ฏธํ™•์ •)" ๊ผฌ๋ฆฌํ‘œ. + return { date: toYmd(addBusinessDays(today, 5)), tentative: true }; +} + +/** ํšŒ์˜๋ก ๋ณธ๋ฌธ์˜ "## 5. ์•ก์…˜ ์•„์ดํ…œ" ๋งˆํฌ๋‹ค์šด ํ‘œ์—์„œ ํ–‰์„ ํŒŒ์‹ฑ. */ +function parseActionItems(report: string): { owner: string; work: string; due: string }[] { + const rows: { owner: string; work: string; due: string }[] = []; + let inSection = false; + for (const line of report.split('\n')) { + if (/^#{1,6}\s*5\.\s*์•ก์…˜\s*์•„์ดํ…œ/.test(line)) { inSection = true; continue; } + if (!inSection) continue; + if (/^#{1,6}\s/.test(line)) break; // ๋‹ค์Œ ์„น์…˜ ์‹œ์ž‘ โ†’ ์ข…๋ฃŒ + if (!/^\s*\|/.test(line)) continue; + const cells = line.split('|').slice(1, -1).map((c) => c.trim()); + if (cells.length < 3) continue; + if (/^:?-+:?$/.test(cells[0])) continue; // ํ‘œ ๊ตฌ๋ถ„์„  + if (cells[0] === '๋‹ด๋‹น' || cells[1] === '์ž‘์—… ๋‚ด์šฉ') continue; // ํ—ค๋” + rows.push({ owner: cells[0], work: cells[1], due: cells[2] }); + } + return rows; +} diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index 482050b..45c51c4 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -35,7 +35,7 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any return true; } logInfo(`[SLASH] handleSlashCommand entering`); - await handleSlashCommand(data.value, provider._view.webview); + await handleSlashCommand(data.value, provider._view.webview, provider._context); logInfo(`[SLASH] handleSlashCommand returned`); return true; }