-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwork-tools.ts
More file actions
105 lines (99 loc) · 4.81 KB
/
Copy pathwork-tools.ts
File metadata and controls
105 lines (99 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* work-tools.ts — pi 확장(extension): 업무 도메인 커스텀 도구 [요구사항 2]
* ============================================================================
*
* pi-agent 철학의 핵심은 "코어(에이전트 루프·세션·LLM 호출)는 pi가 제공하고,
* 우리는 **능력만 확장**한다" 입니다. 그래서 이 파일에는 에이전트 루프가 없습니다.
* 루프는 pi 코어가 돌리고, 우리는 pi에게 '도구 3개'만 등록해 줍니다.
*
* list_notes : notes/ 폴더의 회의록 목록
* read_note : 회의록 한 개 읽기 (경로 탈출 방지)
* write_todo : 정리한 할 일을 todo.md로 저장
*
* 실행:
* pi -e extensions/work-tools.ts --skill skills/todo-briefing -p "회의록 정리해줘"
*
* 도구는 pi.registerTool({ name, description, parameters, execute })로 등록합니다.
* - parameters : typebox의 Type.Object(...) 스키마. 모델은 이 스키마를 보고 호출함.
* - execute : 실제 동작. { content: [...], details } 형태로 결과를 반환.
* 오류는 throw하지 말고 isError:true로 돌려줘서, 모델이 보고
* 스스로 복구(다른 파일명 시도 등)할 수 있게 한다.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
// ── 설정: 입출력 경로 (현재 작업 디렉터리 기준) ──────────────────────────────
// 다른 도메인으로 바꾸려면 이 경로와 아래 도구 3개만 교체하면 됩니다.
const NOTES_DIR = path.resolve(process.cwd(), "notes");
const TODO_FILE = path.resolve(process.cwd(), "todo.md");
/** content 헬퍼: 텍스트 1개짜리 도구 결과를 만든다. */
function textResult(text: string, isError = false) {
return { content: [{ type: "text" as const, text }], details: {}, isError };
}
export default function (pi: ExtensionAPI) {
// ── 도구 1: 회의록 목록 ────────────────────────────────────────────────
pi.registerTool({
name: "list_notes",
label: "List Notes",
description: "notes/ 폴더에 있는 회의록(.md) 파일 목록을 반환한다. 인자는 없다.",
parameters: Type.Object({}),
execute: async () => {
try {
if (!fs.existsSync(NOTES_DIR)) {
return textResult("(notes/ 폴더가 없습니다)");
}
const files = fs
.readdirSync(NOTES_DIR)
.filter((f) => f.endsWith(".md"))
.sort();
return textResult(files.length ? files.join("\n") : "(회의록 파일이 없습니다)");
} catch (e: any) {
return textResult(`[도구 오류] ${e.message}`, true);
}
},
});
// ── 도구 2: 회의록 읽기 (경로 탈출 방지) ───────────────────────────────
pi.registerTool({
name: "read_note",
label: "Read Note",
description: "회의록 파일 하나의 전체 텍스트를 읽어 반환한다.",
parameters: Type.Object({
filename: Type.String({
description: "읽을 파일명. 예: '2026-06-08_주간회의.md'",
}),
}),
execute: async (_id, params) => {
try {
// 경로 조작(../../etc/passwd 등)을 막기 위해 NOTES_DIR 하위인지 검증
const target = path.resolve(NOTES_DIR, params.filename);
if (target !== NOTES_DIR && !target.startsWith(NOTES_DIR + path.sep)) {
return textResult("[도구 오류] notes/ 폴더 밖의 파일은 읽을 수 없습니다.", true);
}
if (!fs.existsSync(target)) {
return textResult(`[도구 오류] 파일을 찾을 수 없습니다: ${params.filename}`, true);
}
return textResult(fs.readFileSync(target, "utf-8"));
} catch (e: any) {
return textResult(`[도구 오류] ${e.message}`, true);
}
},
});
// ── 도구 3: 할 일 저장 ─────────────────────────────────────────────────
pi.registerTool({
name: "write_todo",
label: "Write Todo",
description: "정리한 할 일 목록(마크다운)을 todo.md 파일에 저장한다.",
parameters: Type.Object({
content: Type.String({ description: "todo.md에 쓸 마크다운 전체 내용" }),
}),
execute: async (_id, params) => {
try {
fs.writeFileSync(TODO_FILE, params.content, "utf-8");
return textResult(`todo.md 저장 완료 (${params.content.length}자)`);
} catch (e: any) {
return textResult(`[도구 오류] ${e.message}`, true);
}
},
});
}