CH 18 · 첫 플러그인: hello-plugin
본 장 목표
CH 17에서는 남이 만든 플러그인을 설치하는 법을 배웠습니다. 이 장에서는 직접 플러그인을 작성합니다 — 목표는 최소한입니다: hello-plugin을 작성하고 실제로 dsh가 적재하도록 만들기. 도구는 만들지 않고, 인터페이스도 손대지 않으며, 단 한 가지만 검증합니다: 본인이 작성한 플러그인이 dsh에 의해 발견되고, 적재되고, 실행될 수 있다는 것. 이 한 단계가 연결되면, CH 19부터 CH 23까지의 멋진 내용들(도구, 훅, UI, 발행)이 모두 그 위에서 자라납니다.
먼저 마음가짐 하나: 플러그인은 신비로운 것이 아닙니다. CH 08에서 "모든 것이 플러그인"이라고 말했는데, 이를 뒤집으면 이렇습니다: dsh에 능력을 하나 더 추가하고 싶다면, apply 함수를 export하는 작은 모듈 하나를 작성하세요. 이것이 플러그인의 전부입니다.
실습: hello-plugin 작성하기
원하는 워크스페이스에서 디렉터리를 만듭니다(먼저 해당 디렉터리로 cd한 다음 실행):
New-Item -ItemType Directory -Path "hello-plugin\src" -Force이어서 hello-plugin\src\hello-plugin.js를 만들고 다음과 같이 작성합니다:
export const name = 'hello-plugin'
export function apply(ctx) {
console.log('[hello-plugin] plugin loaded!')
}딱 두 줄의 핵심 로직뿐입니다: 플러그인이 적재되면 [hello-plugin] plugin loaded!를 출력합니다. 이 메시지가 출력된다면, "본인의 코드가 dsh에 의해 실행되었다"는 것이 증명됩니다 — 이것이 첫 번째 milestone입니다.
여기서는 공식 TS 예제 대신 JS를 사용합니다: 전역 설치된 dsh에는 내장 tsx 런타임이 없어서
.ts를 그대로 적재하면 오류가 발생합니다..js를 사용하면 빌드도 필요 없고, 의존성도 없어서 다섯 분이면 됩니다. 더 복잡한 플러그인을 작성할 때는 TypeScript와 빌드 체인을 가져옵니다(CH 20에서 자세히 다룸).
dsh에 적재하기
파일만 있다고 해서 dsh가 알아서 적재하지는 않습니다. "추가로 이 플러그인을 적재하라"고 dsh에게 알려주는 "오버레이"가 필요합니다. hello-plugin\cordis.yml을 만듭니다(아래 name의 경로는 예시이므로 본인 디렉터리로 바꾸고, 뒤에 나오는 주의 사항을 참조하세요):
- insert:
- id: hello
name: 'file:///E:/software-workspace/DeepSeek%20harness%20demo/hello-plugin/src/hello-plugin.js'세 가지 주의 사항:
name은 반드시file://로 시작하는 전체 URL이어야 합니다.E:\...또는E:/...처럼 드라이브 문자로 직접 쓰면 안 됩니다. Windows에서는 dsh의 모듈 로더가file:///E:/...형식만 받아들이며, 드라이브 문자 경로를 직접 쓰면Only URLs with a scheme in: file, data, and node are supported오류가 발생합니다.- 경로에 공백이 있다면
%20으로 인코딩해야 합니다. 예를 들어 디렉터리가my work space라면my%20work%20space로 작성합니다. - 이것은 절대 경로입니다. patch 파일은 설정만 기여하며, 모듈 해석의 루트는 여전히 profile 디렉터리이므로, 로컬 플러그인은 반드시 전체 경로로 작성해야 합니다.
빠른 검증을 위해 headless로 한 번 실행하기
Web UI는 건드리지 말고, headless로 빠르게 플러그인이 실제로 적재되었는지 검증합니다:
cd your-workspace-directory
dsh --profile headless --patch "./hello-plugin/cordis.yml" "Just reply: hi"출력에서 다음 두 줄을 보게 됩니다:
[hello-plugin] plugin loaded!
hi첫 줄은 플러그인이 시작 시점에 출력한 것이고, 두 번째는 작업을 마친 뒤 모델이 보낸 답변입니다. [hello-plugin] plugin loaded!가 보이면, 첫 플러그인이 작동 중인 것입니다.
이어서 Web UI에도 적재하기
headless로 검증은 되었지만, 플러그인은 결국 Web UI에서 사용될 것입니다. 먼저 실행 중인 dsh web을 중단합니다(그렇지 않으면 포트가 점유됨), 그 다음 patch와 함께 시작합니다:
dsh web --patch "./hello-plugin/cordis.yml"http://127.0.0.1:3080을 열면, dsh를 시작한 터미널에도 [hello-plugin] plugin loaded!가 출력됩니다. 현재 hello-plugin에는 UI 효과가 없으며, 그 "출력"은 바로 그 로그 한 줄뿐입니다 — 하지만 이것으로 CH 08에서 다룬 트리의 멤버들과 나란히 web 플러그인 트리에 진입했다는 것이 증명됩니다.

터미널 출력을 보세요: 첫 줄은 dsh 시작 시점에 출력된 플러그인 적재 로그이고, 이어지는 두 줄은 Web UI의 준비 정보입니다.
언로드 시 자동 정리
ctx에 등록한 모든 것(이벤트 리스너, 도구, 타이머)은 플러그인이 언로드될 때 프레임워크가 자동으로 정리해 줍니다. removeListener나 clearInterval를 수동으로 호출할 필요가 없습니다. 만약 수동 해제가 필요한 자원(예: 네트워크 연결)이 있다면, ctx.effect()를 사용해 프레임워크에게 정리 방법을 알려줍니다:
export function apply(ctx) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// 반환된 함수는 플러그인이 언로드될 때 실행됩니다
return () => clearInterval(timer)
})
}effect가 반환한 정리 함수는 플러그인이 언로드되는 순간 호출됩니다 — 이것이 dsh가 자원 라이프사이클을 관리하는 표준 방식입니다.
의존성 선언하기: inject
플러그인이 다른 능력(예: tools, llm)을 필요로 한다면, inject를 선언하면 프레임워크가 본인의 플러그인을 적재하기 전에 의존성이 준비되었음을 보장합니다:
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx) {
// 여기서는 ctx.tools가 사용 가능함이 보장됩니다
ctx.tools.register(/* ... */)
}inject는 Cordis에서 "서비스 의존성"의 진입점입니다. 지금은 이것에 익숙해지고, CH 20에서 도구 플러그인을 작성할 때 공식으로 사용합니다.
플러그인의 세 가지 형태
apply 함수가 가장 흔한 형태이지만, 플러그인은 세 가지 작성 방식을 지원합니다(CH 19에서 각각 자세히 다루며, 여기서는 개요만):
| 형태 | 외형 | 사용 시점 |
|---|---|---|
| 함수 | export function apply(ctx) {} | 기본 선택, 본 튜토리얼에서 사용 |
| 객체 | export default { name, apply(ctx) {} } | 약간의 정적 메타데이터를 함께 담고 싶을 때 |
| 클래스 | export default class extends Service {} | 다른 플러그인에 서비스를 제공하고 싶을 때(CH 19에서 확장) |
지금은 한 줄만 기억하세요: 함수 형태가 90%의 필요를 해결하며, "본인 플러그인이 다른 플러그인의 의존 대상이 되어야 한다"는 분명한 경우에 한해 서비스 형태를 사용하세요.
dsh에게 맡기기: 한 줄 프롬프트로 끝내기
위 과정은 본인이 손으로 직접 진행했고, 모두 배운 내용입니다. 그런데 dsh 자체가 Agent입니다 — 플러그인을 작성하는 일도 수행할 수 있으며, "스스로 작성하고 스스로 검증"합니다.
Web UI의 입력창에 다음 프롬프트를 그대로 보내세요:
현재 워크스페이스에서, dsh가 적재할 수 있는 최소한의 hello-plugin 플러그인을 작성해 줘. 먼저 공식 dsh 플러그인 개발 문서를 읽고, 플러그인이 어떻게 작성되고 적재되어야 하는지 파악한 다음, 공식 명세에 따라 구현하고, 실제로 적재되었는지 검증한 후, 마지막에 결과와 파일 위치를 알려 줘.기술적 세부 사항을 알려 줄 필요 없습니다 — dsh가 공식 플러그인 개발 문서를 스스로 읽고, 어떻게 작성할지, 어떻게 적재할지, 어떻게 검증할지를 스스로 결정합니다. 그저 작업을 지켜보다가, 작성한 내용을 확인하기 위해 파일을 열면 됩니다. 이것이 바로 "모든 것이 플러그인"의 확장입니다: 플러그인을 작성하는 일조차 플러그인으로 조립된 Agent가 수행할 수 있습니다.

저는 실제로 한 번 실행해 봤는데, 우측 상단에 dsh가 정리한 참고 자료 목록과 작성한 파일들(package.json, index.js, cordis.patch.yml)이 표시됩니다. 작성을 마친 뒤, 우측 파일 패널에 워크스페이스에 새로 추가된 hello-plugin 디렉터리가 곧바로 나타납니다 — CH 17에서 설치한 사이드바 플러그인이 빛을 발하는 순간이며, 파일 관리자로 전환해 확인할 필요가 없습니다.
자주 겪는 함정
| 문제 | 현상 | 대응 |
|---|---|---|
Only URLs with a scheme in: file... 오류 보고 | Windows에서 경로를 드라이브 문자 형태로 작성함 | name을 file:///E:/... 같은 전체 URL로 변경 |
| 파일/모듈을 찾을 수 없다고 보고됨 | 경로의 공백이 인코딩되지 않음 | 공백을 %20으로 작성 |
| patch와 재시작 후에도 응답이 없음? | 플러그인 경로 또는 yml의 오타 | id와 name의 철자 확인, dsh --profile web --patch ./hello-plugin/cordis.yml --dump-config로 설정 트리에 플러그인이 있는지 확인 |
.ts를 그대로 적재하면 오류? | 전역 dsh에는 내장 tsx 런타임이 없음 | 우선 .js로 빌드 없이 실행하고, TS가 필요해지면 빌드해 .js로 만든 다음 적재 |
| 포트가 점유되어 시작할 수 없음? | 이전 dsh web이 여전히 실행 중 | 기존 프로세스를 먼저 중단한 다음 시작 |
이 장에서 배운 것
아래 항목들을 직접 해낼 수 있으면 합격입니다:
- [ ] 플러그인의 최소 형태를 말할 수 있다:
apply(ctx)함수를 export하는 모듈 - [ ] hello-plugin을 만들고
cordis.yml에서file://URL로 선언 - [ ]
dsh --profile headless --patch ...로 플러그인이 적재되었는지 빠르게 검증 - [ ] web을
--patch로 시작해 플러그인을 Web UI의 플러그인 트리로 가져오기 - [ ]
ctx.effect()가 자원을 정리함을 알고,inject가 서비스 의존성을 선언함을 알며, 플러그인에는 함수 / 객체 / 클래스 세 가지 형태가 있음을 안다
