@morit/cli 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/assets/plugin_contract.json +1 -0
- package/bin/morit.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +5 -0
- package/src/workspace.js +60 -8
- package/assets/docs/README.md +0 -107
- package/assets/docs/ai-response-and-timeline.md +0 -211
- package/assets/docs/ai-skill-and-docx-workflow.md +0 -83
- package/assets/docs/app-builder.md +0 -56
- package/assets/docs/authentication.md +0 -140
- package/assets/docs/components.md +0 -225
- package/assets/docs/design-tokens-responsive.md +0 -171
- package/assets/docs/docs-index.json +0 -94
- package/assets/docs/examples-notion.md +0 -83
- package/assets/docs/examples-school-life.md +0 -79
- package/assets/docs/getting-started.md +0 -132
- package/assets/docs/information-hierarchy.md +0 -81
- package/assets/docs/instances-and-connectors.md +0 -93
- package/assets/docs/lifecycle-and-api.md +0 -169
- package/assets/docs/local-cli.md +0 -125
- package/assets/docs/manifest.md +0 -247
- package/assets/docs/packaging-and-testing.md +0 -131
- package/assets/docs/permissions-and-data.md +0 -149
- package/assets/docs/platform-compatibility.md +0 -62
- package/assets/docs/plugin-storage.md +0 -175
- package/assets/docs/project-structure.md +0 -102
- package/assets/docs/remote-mcp.md +0 -158
- package/assets/docs/school-life-privacy.md +0 -55
- package/assets/docs/screens-layout-navigation.md +0 -95
- package/assets/docs/sdk-and-mcp.md +0 -199
- package/assets/docs/tool-and-skill.md +0 -188
- package/assets/docs/troubleshooting.md +0 -121
- package/assets/docs/ui-extensions.md +0 -75
- package/assets/docs/ui-runtime-v2.md +0 -406
- package/assets/docs/verification.md +0 -133
package/README.md
CHANGED
|
@@ -46,11 +46,9 @@ npx -y @morit/cli plugin sync . --pull # Cloud → local
|
|
|
46
46
|
## SDK와 문서
|
|
47
47
|
|
|
48
48
|
Node SDK는 `@morit/cli/sdk` export에서 같은 contract, validator, preview, build 기능을 제공합니다.
|
|
49
|
-
배포 package
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Plugin MCP 설정과 Local/Remote Tool 흐름은 bundled `assets/docs/sdk-and-mcp.md`, manifest와 UI
|
|
53
|
-
계약은 `assets/docs/manifest.md`, `assets/docs/ui-runtime-v2.md`를 참고하세요.
|
|
49
|
+
배포 package에는 `assets/plugin_contract.json` 계약이 들어갑니다. 개발 문서는
|
|
50
|
+
`https://developers.moring.co/docs/morit-plugin/start`가 canonical 원본이며 Local MCP의
|
|
51
|
+
`morit_docs_search`와 `morit_docs_get`도 같은 원본을 ETag 캐시로 읽습니다.
|
|
54
52
|
|
|
55
53
|
## 파일과 보안 경계
|
|
56
54
|
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"slash_commands": {"target": "slash_commands", "kind": null}
|
|
46
46
|
},
|
|
47
47
|
"object_fields": {
|
|
48
|
+
"http_json_response": ["collection_path", "id_path", "url_path", "title_paths", "content_paths", "max_items", "summary_prefix", "followup", "followups", "summary_path", "data_path"],
|
|
48
49
|
"capability": ["id", "kind", "title", "description", "permissions", "timeout_seconds", "runtime", "input_schema", "output_schema"],
|
|
49
50
|
"ui_extension": ["id", "point", "title", "order", "permissions", "config"],
|
|
50
51
|
"credential": ["id", "label", "description", "kind", "required", "oauth_provider", "allow_multiple"],
|
package/bin/morit.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ const HELP = `Official Morit Developer CLI
|
|
|
19
19
|
Usage:
|
|
20
20
|
morit login [--token <access-token>] [--api-url <origin>]
|
|
21
21
|
morit logout
|
|
22
|
+
morit docs
|
|
22
23
|
morit plugin setup [directory] --id <plugin.id> --name <name> --publisher <publisher>
|
|
23
24
|
morit plugin add [directory]
|
|
24
25
|
morit plugin validate [directory]
|
|
@@ -348,6 +349,10 @@ export async function runCli(argv, options = {}) {
|
|
|
348
349
|
output(stdout, "Signed out of Morit CLI");
|
|
349
350
|
return 0;
|
|
350
351
|
}
|
|
352
|
+
if (area === "docs") {
|
|
353
|
+
output(stdout, { url: `${DEFAULT_API_URL}/docs/morit-plugin/start` }, argv.includes("--json"));
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
351
356
|
const parsed = parseArguments(rest);
|
|
352
357
|
const json = Boolean(parsed.flags.json);
|
|
353
358
|
|
package/src/workspace.js
CHANGED
|
@@ -727,10 +727,10 @@ async function signingKeyForPublisher(keyDirectory, publisher) {
|
|
|
727
727
|
}
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
-
function previewHtml(manifest) {
|
|
731
|
-
return renderPreviewHtml(manifest);
|
|
732
|
-
}
|
|
733
|
-
|
|
730
|
+
function previewHtml(manifest) {
|
|
731
|
+
return renderPreviewHtml(manifest);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
734
|
function validateMetadata(pluginId, name, publisher, description) {
|
|
735
735
|
if (!PLUGIN_ID.test(pluginId) || pluginId.length > 120) throw new Error("plugin_id must be a reverse-domain lowercase identifier");
|
|
736
736
|
if (typeof name !== "string" || name.trim().length < 1 || name.trim().length > 80) throw new Error("name must contain 1 to 80 characters");
|
|
@@ -1188,6 +1188,54 @@ function validateSlashCommands(commands, capabilities) {
|
|
|
1188
1188
|
}
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
function validateHttpRuntime(runtime, endpoint) {
|
|
1192
|
+
let url;
|
|
1193
|
+
try { url = new URL(endpoint); } catch { throw new Error("http_json requires a valid HTTPS endpoint"); }
|
|
1194
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443") || endpoint.length > 2048) {
|
|
1195
|
+
throw new Error("http_json requires a valid HTTPS endpoint");
|
|
1196
|
+
}
|
|
1197
|
+
if (!["GET", "POST"].includes(String(runtime.method || "POST").toUpperCase())) {
|
|
1198
|
+
throw new Error("http_json supports GET and POST");
|
|
1199
|
+
}
|
|
1200
|
+
if (runtime.request_body != null) assertObject(runtime.request_body, "http_json request_body");
|
|
1201
|
+
if (runtime.request_params != null) {
|
|
1202
|
+
const params = runtime.request_params;
|
|
1203
|
+
assertObject(params, "http_json request_params");
|
|
1204
|
+
if (Object.keys(params).length > 32) throw new Error("http_json request_params accepts at most 32 parameters");
|
|
1205
|
+
const entries = [];
|
|
1206
|
+
for (const [name, value] of Object.entries(params)) {
|
|
1207
|
+
if (!name.length || name.length > 80 || /[\x00-\x1f]/.test(name)) throw new Error("invalid http_json parameter name");
|
|
1208
|
+
const values = Array.isArray(value) ? value : [value];
|
|
1209
|
+
if (values.length > 64 || values.some(item => item != null && (
|
|
1210
|
+
!["string", "number", "boolean"].includes(typeof item) || typeof item === "number" && !Number.isFinite(item)
|
|
1211
|
+
))) throw new Error("http_json parameters must be scalars or scalar lists");
|
|
1212
|
+
for (const item of values) if (item != null) entries.push([name, String(item)]);
|
|
1213
|
+
}
|
|
1214
|
+
if (new URLSearchParams(entries).toString().length > 16 * 1024) throw new Error("http_json request_params is too large");
|
|
1215
|
+
}
|
|
1216
|
+
const response = runtime.response || {};
|
|
1217
|
+
rejectUnknownFields(response, new Set(contract.object_fields.http_json_response), "http_json response");
|
|
1218
|
+
const projection = "summary_path" in response || "data_path" in response;
|
|
1219
|
+
if (projection && (Object.keys(response).some(name => !["summary_path", "data_path", "summary_prefix"].includes(name)) ||
|
|
1220
|
+
!("summary_path" in response) && !("summary_prefix" in response))) {
|
|
1221
|
+
throw new Error("http_json cannot mix object and collection response settings");
|
|
1222
|
+
}
|
|
1223
|
+
const validPath = value => typeof value === "string" && value.length > 0 && value.length <= 240 && value.split(".").every(part => /^(?:\*|[A-Za-z0-9_-]{1,80})$/.test(part));
|
|
1224
|
+
for (const name of ["summary_path", "data_path", "collection_path", "id_path", "url_path"]) {
|
|
1225
|
+
if (name in response && (!validPath(response[name]) || ["summary_path", "data_path"].includes(name) && response[name].split(".").includes("*"))) {
|
|
1226
|
+
throw new Error(`invalid http_json response.${name}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
for (const name of ["title_paths", "content_paths"]) {
|
|
1230
|
+
if (name in response && (!Array.isArray(response[name]) || response[name].length > 24 || response[name].some(path => !validPath(path)))) {
|
|
1231
|
+
throw new Error(`invalid http_json response.${name}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if ("max_items" in response && (!Number.isInteger(response.max_items) || response.max_items < 1 || response.max_items > 20)) {
|
|
1235
|
+
throw new Error("invalid http_json response.max_items");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1191
1239
|
function validateRuntime(capability, runtime, permissions, credentials, connectors, dependencies) {
|
|
1192
1240
|
const adapter = runtime.adapter;
|
|
1193
1241
|
const credentialIds = new Set(credentials.map((value) => value.id));
|
|
@@ -1198,9 +1246,13 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
|
|
|
1198
1246
|
if (adapter === "text_template" && (typeof runtime.template !== "string" || !runtime.template.trim() || runtime.template.length > 2000)) {
|
|
1199
1247
|
throw new Error("text_template requires a bounded template");
|
|
1200
1248
|
}
|
|
1201
|
-
if (adapter === "http_json"
|
|
1202
|
-
|
|
1203
|
-
|
|
1249
|
+
if (adapter === "http_json") {
|
|
1250
|
+
if (!permissions.has("network")) throw new Error("http_json requires network permission");
|
|
1251
|
+
if (effectiveCredential && (!credentialIds.has(effectiveCredential) || !permissions.has("credentials"))) {
|
|
1252
|
+
throw new Error("http_json requires a declared credential");
|
|
1253
|
+
}
|
|
1254
|
+
validateHttpRuntime(runtime, effectiveEndpoint);
|
|
1255
|
+
}
|
|
1204
1256
|
if (adapter === "mcp_http") {
|
|
1205
1257
|
if (!permissions.has("network")) throw new Error("mcp_http requires network permission");
|
|
1206
1258
|
if (effectiveCredential && (!credentialIds.has(effectiveCredential) || !permissions.has("credentials"))) throw new Error("mcp_http requires a declared credential");
|
|
@@ -1218,7 +1270,7 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
|
|
|
1218
1270
|
}
|
|
1219
1271
|
if (adapter === "neis_school") {
|
|
1220
1272
|
if (!permissions.has("network") || !permissions.has("storage")) throw new Error("neis_school requires network and storage permissions");
|
|
1221
|
-
if (!["setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
|
|
1273
|
+
if (!["school_search", "setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
|
|
1222
1274
|
if (["reminder", "briefing"].includes(runtime.operation) && !permissions.has("notifications")) throw new Error("NEIS reminder requires notifications permission");
|
|
1223
1275
|
}
|
|
1224
1276
|
if (capability.kind === "provider" && (runtime.role !== "search" || !adapter)) throw new Error("provider capabilities must declare a search runtime");
|
package/assets/docs/README.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Morit Plugin 개발 문서
|
|
2
|
-
|
|
3
|
-
이 문서는 아이디어를 실제 설치 가능한 `.mplg`로 만드는 순서대로 구성되어 있습니다. 플러그인은
|
|
4
|
-
실행 코드를 앱에 직접 주입하지 않습니다. Manifest와 JSON fragment로 기능·화면·권한을 선언하고,
|
|
5
|
-
Morit Host가 서명과 계약을 확인한 뒤 공용 런타임으로 실행합니다.
|
|
6
|
-
|
|
7
|
-
## 가장 짧은 개발 경로
|
|
8
|
-
|
|
9
|
-
```text
|
|
10
|
-
요구사항 정리
|
|
11
|
-
→ 프로젝트 생성
|
|
12
|
-
→ manifest와 기능 fragment 작성
|
|
13
|
-
→ 화면·상태·내비게이션 작성
|
|
14
|
-
→ 권한·설정·알림 연결
|
|
15
|
-
→ validate
|
|
16
|
-
→ preview
|
|
17
|
-
→ build
|
|
18
|
-
→ verify
|
|
19
|
-
→ 실제 앱 설치·실사용 테스트
|
|
20
|
-
→ deploy
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
공식 CLI를 사용하는 기본 명령은 다음과 같습니다.
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx -y @morit/cli plugin setup . \
|
|
27
|
-
--id com.example.study \
|
|
28
|
-
--name "Study" \
|
|
29
|
-
--publisher example
|
|
30
|
-
npx -y @morit/cli plugin validate .
|
|
31
|
-
npx -y @morit/cli plugin preview . --output ./dist/preview.html
|
|
32
|
-
npx -y @morit/cli plugin build .
|
|
33
|
-
npx -y @morit/cli login
|
|
34
|
-
npx -y @morit/cli plugin add .
|
|
35
|
-
npx -y @morit/cli plugin deploy . --visibility private
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
같은 safe renderer는 공식 CLI, Local/Remote Plugin MCP의 `morit_project_preview`, 저장소 도구
|
|
39
|
-
`python tools/morit_plugin.py preview <project>`에서 사용합니다. preview의 라이트·다크 전환으로
|
|
40
|
-
계약과 배치를 확인하고, 실제 Flutter focus·navigation·Tool 흐름은 앱에서도 확인합니다.
|
|
41
|
-
|
|
42
|
-
## 개발 순서별 인덱스
|
|
43
|
-
|
|
44
|
-
### 1. 시작과 개발 흐름
|
|
45
|
-
|
|
46
|
-
1. [시작하기와 개발 흐름](getting-started.md) — 개발 방식 선택, 첫 프로젝트, 완료 조건
|
|
47
|
-
2. [앱에서 플러그인 만들기](app-builder.md) — 단순 Tool을 앱 Builder로 만드는 범위
|
|
48
|
-
|
|
49
|
-
### 2. 프로젝트와 계약
|
|
50
|
-
|
|
51
|
-
3. [프로젝트 구조와 fragment](project-structure.md) — 디렉터리, 병합 규칙, 패키지 포함 파일
|
|
52
|
-
4. [Manifest 레퍼런스](manifest.md) — ID, 버전, capability, connector, dependency
|
|
53
|
-
5. [Instance, Connector, 복합 패키지](instances-and-connectors.md) — 사용자별 실행 단위와 계정 연결
|
|
54
|
-
|
|
55
|
-
### 3. 화면과 사용자 경험
|
|
56
|
-
|
|
57
|
-
6. [화면, 레이아웃, 내비게이션](screens-layout-navigation.md) — 화면 역할과 이동 구조
|
|
58
|
-
7. [기본·커스텀 컴포넌트](components.md) — 각 node의 목적, 속성, 제약
|
|
59
|
-
8. [토큰, 크기, 색, 여백, 반응형](design-tokens-responsive.md) — Material 3 기반 시각 규칙
|
|
60
|
-
9. [화면 분리와 정보 계층](information-hierarchy.md) — 한 화면에 정보를 몰지 않는 설계
|
|
61
|
-
10. [UI extension point](ui-extensions.md) — Host의 어느 위치에 UI를 노출할지 선택
|
|
62
|
-
11. [UI Runtime v2 레퍼런스](ui-runtime-v2.md) — data, state, binding, event 계약
|
|
63
|
-
12. [Response UI와 Agent Timeline](ai-response-and-timeline.md) — AI 메시지 안의 결과 UI
|
|
64
|
-
|
|
65
|
-
### 4. 기능, 데이터, 사용자 제어
|
|
66
|
-
|
|
67
|
-
13. [Tool, Skill, Search, Slash Command](tool-and-skill.md) — capability와 runtime adapter
|
|
68
|
-
14. [권한, 설정, 저장소, 알림](permissions-and-data.md) — 최소 권한과 Host action
|
|
69
|
-
15. [Plugin Local Storage와 AI 접근](plugin-storage.md) — namespace, CRUD, migration, 격리, AI Tool
|
|
70
|
-
16. [외부 서비스 인증과 Cloud Secrets](authentication.md) — OAuth, API key, 비밀 값 경계
|
|
71
|
-
17. [AI Skill과 파일 산출물](ai-skill-and-docx-workflow.md) — 실제 파일을 반환하는 완료 흐름
|
|
72
|
-
|
|
73
|
-
### 5. 플랫폼
|
|
74
|
-
|
|
75
|
-
18. [Android, iOS, Desktop 호환](platform-compatibility.md) — 현재 지원 범위와 이식 원칙
|
|
76
|
-
|
|
77
|
-
### 6. 검증과 배포
|
|
78
|
-
|
|
79
|
-
19. [공식 CLI 개발 흐름](local-cli.md) — setup, sync, preview, build, deploy
|
|
80
|
-
20. [패키징과 테스트](packaging-and-testing.md) — 서명, 무결성, 테스트 층
|
|
81
|
-
21. [오류 해결](troubleshooting.md) — validate, preview, build, 설치, 실행 오류 구분
|
|
82
|
-
22. [예제 검증 방법과 확인 경계](verification.md) — 자동·live·실기기 검증의 차이
|
|
83
|
-
|
|
84
|
-
### 7. SDK, MCP, API
|
|
85
|
-
|
|
86
|
-
23. [CLI와 AI 에이전트 MCP](sdk-and-mcp.md) — Codex·Claude 등 로컬/원격 연결
|
|
87
|
-
24. [원격 Plugin MCP](remote-mcp.md) — Cloud Project를 다루는 Tool 계약
|
|
88
|
-
25. [수명주기와 HTTP API](lifecycle-and-api.md) — Host API와 설치·실행 상태
|
|
89
|
-
|
|
90
|
-
### 8. 실제 예제
|
|
91
|
-
|
|
92
|
-
26. [학교 생활 플러그인](examples-school-life.md) — 공개 NEIS와 AI 할 일을 사용하는 학생용 경험
|
|
93
|
-
27. [학교 생활 개인정보 처리](school-life-privacy.md) — 저장·전송·삭제 범위
|
|
94
|
-
28. [Notion 플러그인](examples-notion.md) — OAuth Connection과 실제 문서 검색
|
|
95
|
-
|
|
96
|
-
## 문서가 배포되는 위치
|
|
97
|
-
|
|
98
|
-
`docs/plugin_docs`가 문서 콘텐츠의 공통 원본입니다. 같은 파일이 다음 위치에 복사되거나 빌드 시
|
|
99
|
-
포함됩니다.
|
|
100
|
-
|
|
101
|
-
- `@morit/cli`: npm 패키지의 `assets/docs`
|
|
102
|
-
- Local MCP `@morit/plugin-mcp`: npm 패키지의 `assets/docs`
|
|
103
|
-
- Remote MCP: 서비스 이미지의 `/app/docs/plugin_docs`
|
|
104
|
-
- `developers.moring.co`: `docs-index.json`의 순서와 경로로 생성한 MDX
|
|
105
|
-
|
|
106
|
-
네 배포 위치는 같은 계약과 예제를 제공하며, 문서의 JSON은 공식 CLI와 Host 계약으로 지속
|
|
107
|
-
검증합니다. 각 경로에서 별도 규격을 정의하지 않습니다.
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
# Response UI와 Agent Timeline
|
|
2
|
-
|
|
3
|
-
Response UI는 capability 결과를 AI 메시지 안에서 시각화합니다. AI는 필요한 A2UI
|
|
4
|
-
Native Tool을 선택하지만 Host는 우선 최종 텍스트 답변을 완료·저장합니다. 컴포넌트는
|
|
5
|
-
데이터 준비와 검증이 모두 성공한 뒤 해당 AI 메시지 본문 아래에 한 번에 자연스럽게
|
|
6
|
-
표시됩니다. Tool이 실패해도 답변 완료 상태는 바뀌지 않습니다.
|
|
7
|
-
|
|
8
|
-
## Response extension
|
|
9
|
-
|
|
10
|
-
```json
|
|
11
|
-
{
|
|
12
|
-
"id": "school_life.day_response",
|
|
13
|
-
"point": "response",
|
|
14
|
-
"title": "오늘 학교 생활",
|
|
15
|
-
"order": 10,
|
|
16
|
-
"permissions": [],
|
|
17
|
-
"config": {
|
|
18
|
-
"ui_schema": 2,
|
|
19
|
-
"a2ui": {
|
|
20
|
-
"version": "v0.9",
|
|
21
|
-
"description": "수업 정보가 실제 답변에 필요하고 표시할 항목이 있을 때만 사용합니다.",
|
|
22
|
-
"schema": {
|
|
23
|
-
"type": "object",
|
|
24
|
-
"properties": {
|
|
25
|
-
"timetable": {
|
|
26
|
-
"type": "array",
|
|
27
|
-
"items": {"type": "object"},
|
|
28
|
-
"minItems": 1,
|
|
29
|
-
"maxItems": 12
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"required": ["timetable"],
|
|
33
|
-
"additionalProperties": true
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"theme": {"density": "compact"},
|
|
37
|
-
"data_sources": [
|
|
38
|
-
{
|
|
39
|
-
"id": "result",
|
|
40
|
-
"capability": "school_life.schedule.lookup",
|
|
41
|
-
"trigger": "manual",
|
|
42
|
-
"query": "",
|
|
43
|
-
"arguments": {}
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"view": {
|
|
47
|
-
"type": "column",
|
|
48
|
-
"props": {"spacing": 10},
|
|
49
|
-
"children": [
|
|
50
|
-
{
|
|
51
|
-
"type": "text",
|
|
52
|
-
"props": {"text": "{{data.result.summary}}", "style": "heading"}
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"type": "timeline",
|
|
56
|
-
"props": {"source": "data.result.data.timetable", "empty_text": "수업 정보가 없습니다."},
|
|
57
|
-
"children": [
|
|
58
|
-
{
|
|
59
|
-
"type": "text",
|
|
60
|
-
"props": {"text": "{{item.period}}교시 · {{item.subject}}"}
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Response point도 일반 Runtime v2와 같은 node, theme, app bar, navigation, binding, action 계약을
|
|
71
|
-
사용합니다. Host가 선택·검증한 capability 결과는 `data` namespace에 주입되므로 같은 결과를 얻기
|
|
72
|
-
위해 화면 mount 시 외부 요청을 반복하지 않습니다. 후속 refresh나 사용자가 누른 action에만 추가
|
|
73
|
-
capability를 호출합니다. 기존 `a2ui` 선언이 없는 Response extension은 지원하지 않습니다.
|
|
74
|
-
|
|
75
|
-
## AI 선택과 Host 검증
|
|
76
|
-
|
|
77
|
-
서버는 모든 Tool 결과에 UI를 일괄 생성하거나 키워드 정규식으로 컴포넌트를 강제하지 않습니다.
|
|
78
|
-
최종 답변 모델은 통합 Registry Skill의 용도와 입력 Schema를 의미적으로 비교해 Native Tool을
|
|
79
|
-
선택합니다. provider 컴포넌트에는 위치·통화·검색어만 넘기고, Host가 비동기로 실시간 데이터를
|
|
80
|
-
조회합니다. generated 컴포넌트에는 답변에 근거한 차트 값만 Schema에 맞게
|
|
81
|
-
전달합니다. Plugin 결과는 Host가 검증한 후보 데이터만 사용합니다.
|
|
82
|
-
|
|
83
|
-
선택 뒤 Host는 다음을 모두 통과한 항목만 A2UI v0.9 DataPart로 저장하고 해당 AI 메시지에 붙입니다.
|
|
84
|
-
|
|
85
|
-
- 실행 상태가 `completed` 또는 `partial`이고 실제 값이 있음
|
|
86
|
-
- 질문·최종 답변과 capability 결과가 관련됨
|
|
87
|
-
- 같은 데이터의 Response UI가 이미 선택되지 않음
|
|
88
|
-
- 기본 컴포넌트별 필수 값과 URL 형식이 유효함
|
|
89
|
-
- Plugin 컴포넌트의 schema, A2UI version, 설치 상태, Instance 권한과 capability 연결이 유효함
|
|
90
|
-
|
|
91
|
-
빈 데이터, 실패한 Tool, 무관하거나 중복된 결과, 불확실한 선택은 UI를 만들지 않습니다. 선택 모델
|
|
92
|
-
호출이나 렌더링이 실패해도 일반 텍스트 답변은 그대로 완료됩니다. 로딩 문구,
|
|
93
|
-
skeleton, 빈 card는 렌더링하지 않고 준비가 끝난 컴포넌트만 한 번 표시합니다.
|
|
94
|
-
|
|
95
|
-
## 기본 A2UI Component Catalog
|
|
96
|
-
|
|
97
|
-
| component | 표시 조건 |
|
|
98
|
-
|---|---|
|
|
99
|
-
| `weather` | 현재 기온·상태 또는 예보가 하나 이상 유효함 |
|
|
100
|
-
| `world_clock` | 1~8개 위치의 IANA timezone과 현재 시각이 유효함 |
|
|
101
|
-
| `chart` | label과 수치가 있는 행이 2개 이상임; `bar`, `line`, `pie`, `scatter` |
|
|
102
|
-
| `image_gallery` | HTTPS 이미지와 원문 URL·출처명이 함께 있음 |
|
|
103
|
-
| `exchange_rate` | 기준 통화, 상대 통화, 유한한 환율 값이 있음 |
|
|
104
|
-
| `article_list` | 제목·출처·원문이 있는 서로 다른 기사 2개 이상 |
|
|
105
|
-
| `article_card` | 제목·출처·요약·본문 일부·원문이 있고, 게시 시각·이미지는 제공될 때 표시 |
|
|
106
|
-
| `file_result` | 실제 Morit item ID 또는 검증 가능한 다운로드 URL이 있음 |
|
|
107
|
-
| `image_preview` | 실제 image item 또는 검증 가능한 preview URL이 있음 |
|
|
108
|
-
| `video_preview` | 실제 video item/URL과 thumbnail URL이 함께 있음 |
|
|
109
|
-
|
|
110
|
-
`weather`, `exchange_rate`, `world_clock`, `article_list`, `article_card`, `image_gallery`는
|
|
111
|
-
provider mode이고 `chart`는 generated mode입니다. 기사와 이미지는 페이지 이동·다시 시도,
|
|
112
|
-
gallery는 반응형 grid/carousel을 지원합니다. 표는 일반 Markdown으로 답변하고 단계별
|
|
113
|
-
텍스트 slider는 catalog에서 제거되었습니다.
|
|
114
|
-
각 renderer도 같은 필수 값을 다시 확인하고, 값이 바뀌거나 손상되면 해당 컴포넌트만 fallback합니다.
|
|
115
|
-
|
|
116
|
-
## Plugin 컴포넌트의 동적 등록
|
|
117
|
-
|
|
118
|
-
활성 Instance의 `point: "response"` extension은 capability 실행 시
|
|
119
|
-
`plugin.<extension-id>` 이름으로 현재 A2UI catalog에 동적 등록됩니다. AI에게는 Host가 확인한 ID,
|
|
120
|
-
설명, version, schema만 노출됩니다. `a2ui.schema`는 root `object`인 안전한 JSON Schema 부분집합이며
|
|
121
|
-
`type`, `properties`, `required`, `additionalProperties`, `items`, `enum`, 문자열·숫자·배열 bound를
|
|
122
|
-
지원합니다. schema는 최대 16 KiB, 깊이 6이며 object/list는 각 64개로 제한됩니다.
|
|
123
|
-
|
|
124
|
-
Plugin Response는 Runtime v2의 전체 layout, surface, theme, image, action과 binding을 사용할 수
|
|
125
|
-
있습니다. 단, extension 권한은 Manifest grant의 부분집합이어야 하고 `data_sources` 중 하나가 실제
|
|
126
|
-
실행 capability를 가리켜야 합니다. 패키지 설치, CLI validate, Host 로드, Flutter parse와 표시 직전
|
|
127
|
-
검증이 같은 규칙을 사용합니다.
|
|
128
|
-
|
|
129
|
-
## 한 container의 정보 순서
|
|
130
|
-
|
|
131
|
-
1. 결과를 설명하는 짧은 제목 또는 summary
|
|
132
|
-
2. 사용자가 요청한 핵심 데이터
|
|
133
|
-
3. 출처·기간·갱신 시점 같은 보조 정보
|
|
134
|
-
4. 필요한 후속 행동 1~2개
|
|
135
|
-
|
|
136
|
-
복사, 다시 시도, 다운로드 같은 메뉴도 같은 container의 Host chrome에 속합니다. 각 section을
|
|
137
|
-
별도 떠 있는 card로 만들지 않습니다. 긴 결과는 list limit과 상세 화면 이동을 사용합니다.
|
|
138
|
-
|
|
139
|
-
## 대화 UI를 깨뜨리지 않는 제약
|
|
140
|
-
|
|
141
|
-
- 메시지 폭을 넘는 고정 width를 사용하지 않습니다.
|
|
142
|
-
- Response 내부에 자체 채팅 입력창을 만들지 않습니다.
|
|
143
|
-
- 무한 높이 목록 대신 요약과 상세 화면 이동을 제공합니다.
|
|
144
|
-
- background refresh가 대화 scroll 위치를 바꾸지 않게 기존 높이와 데이터를 가능한 유지합니다.
|
|
145
|
-
- 렌더링 오류는 해당 Response UI를 생략하고 이미 완료된 텍스트 답변을 유지합니다.
|
|
146
|
-
- accessibility 순서는 AI 본문 다음, Response 제목, 내용, 행동 순으로 유지합니다.
|
|
147
|
-
|
|
148
|
-
## Text fallback
|
|
149
|
-
|
|
150
|
-
Response UI를 표시할 수 없더라도 AI의 원래 텍스트 답변은 남아야 합니다. capability는 UI 전용
|
|
151
|
-
원시 JSON만 반환하지 말고 사람이 읽을 수 있는 `summary`를 함께 제공합니다. 빈 배열만 있거나
|
|
152
|
-
schema가 맞지 않으면 UI를 억지로 채우지 않습니다. 알 수 없는 node, 손상된 binding, 이미지 로드
|
|
153
|
-
실패는 전체 답변 성공을 숨기지 않습니다.
|
|
154
|
-
|
|
155
|
-
```json
|
|
156
|
-
{
|
|
157
|
-
"completed": true,
|
|
158
|
-
"summary": "오늘은 6교시이며 점심은 카레라이스입니다.",
|
|
159
|
-
"data": {
|
|
160
|
-
"timetable": [],
|
|
161
|
-
"meal": {}
|
|
162
|
-
},
|
|
163
|
-
"evidence": []
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Agent Timeline
|
|
168
|
-
|
|
169
|
-
Agent Timeline은 모델의 숨겨진 추론이 아니라 사용자가 이해할 수 있는 실행 상태만 보여줍니다.
|
|
170
|
-
|
|
171
|
-
```text
|
|
172
|
-
요청을 확인하고 계획했어요
|
|
173
|
-
├─ 학교 정보를 확인했어요
|
|
174
|
-
├─ 오늘 시간표를 불러왔어요
|
|
175
|
-
└─ 결과를 정리했어요
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Timeline event에는 도구 이름이나 내부 stack trace 대신 작업 이름, 상태, 필요한 사용자 행동을
|
|
179
|
-
기록합니다. 상태는 대체로 다음과 같습니다.
|
|
180
|
-
|
|
181
|
-
- 진행 중
|
|
182
|
-
- 완료
|
|
183
|
-
- 재시도 중
|
|
184
|
-
- 사용자 입력 필요
|
|
185
|
-
- 일부 결과로 완료
|
|
186
|
-
- 실패
|
|
187
|
-
|
|
188
|
-
Tool 하나가 실패했지만 대체 경로로 결과를 만들었다면 전체 timeline을 실패로 표시하지 않습니다.
|
|
189
|
-
실패한 단계와 복구 결과를 함께 설명합니다.
|
|
190
|
-
|
|
191
|
-
## Code Interpreter 파일
|
|
192
|
-
|
|
193
|
-
Code Interpreter가 파일을 만들었다면 sandbox 내부 경로만 답변에 남기지 않습니다. 파일은 Host가
|
|
194
|
-
접근 가능한 artifact로 전달되고, AI 답변에는 실제 파일 이름·형식·크기와 다운로드 가능한 링크가
|
|
195
|
-
있어야 합니다. Response UI의 다운로드 action은 동일 artifact를 가리키며 존재하지 않는 경로나
|
|
196
|
-
가짜 링크를 생성하지 않습니다.
|
|
197
|
-
|
|
198
|
-
파일 생성 완료 조건은 [AI Skill과 파일 산출물](ai-skill-and-docx-workflow.md)에 정리되어 있습니다.
|
|
199
|
-
|
|
200
|
-
## 이미지 artifact
|
|
201
|
-
|
|
202
|
-
`image_generation`·`image_edit`의 PNG/JPEG/GIF/WebP artifact는 생성 중 placeholder에서 완료 후 답변
|
|
203
|
-
내 inline 이미지로 바뀝니다. 여러 장은 gallery로 넘기며 원본 비율을 유지한 contain 렌더링,
|
|
204
|
-
전체 화면 `InteractiveViewer`, 다운로드, Android 공유를 제공합니다. 대화 재진입 때도 저장된
|
|
205
|
-
`ai_tool_executions.result_summary.artifacts`를 다시 사용합니다.
|
|
206
|
-
|
|
207
|
-
Artifact URL은 conversation·execution·attachment 소유권을 확인한 뒤 5분 signed URL로 만들며 만료나
|
|
208
|
-
일시 실패 시 새 URL을 받아 두 번 시도합니다. 앱은 redirect를 따르지 않고 HTTPS(개발 loopback 제외),
|
|
209
|
-
MIME, magic byte, 실제 decode, 24 MiB, 4096 px, 16 MP를 확인합니다. SVG와 외부 실행 형식은 inline으로
|
|
210
|
-
열지 않고 기존 파일 카드로 fallback합니다. Plugin response의 URL 이미지는 앱이 직접 요청하지 않고
|
|
211
|
-
기존 인증 Host proxy와 `network` grant를 거치며 실패 시 이미지 단위 재시도를 제공합니다.
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# AI Skill과 파일 산출물
|
|
2
|
-
|
|
3
|
-
Morit Plugin의 `kind: "skill"`과 Codex·Claude 같은 AI 에이전트의 Skill 파일은 이름이 같지만
|
|
4
|
-
역할이 다릅니다.
|
|
5
|
-
|
|
6
|
-
- Morit Plugin Skill: Host가 설치·권한·timeout 경계에서 실행하는 capability
|
|
7
|
-
- AI 에이전트 Skill: 에이전트가 작업 순서와 도구 사용법을 따르는 지침
|
|
8
|
-
|
|
9
|
-
에이전트 Skill을 사용해 플러그인을 만들더라도 결과 `.mplg`는 일반 SDK 계약과 동일하게 검증·서명됩니다.
|
|
10
|
-
|
|
11
|
-
## 파일 작업 완료 순서
|
|
12
|
-
|
|
13
|
-
문서, 표, 이미지, archive 등 파일을 만드는 capability나 AI 작업은 다음 순서를 지킵니다.
|
|
14
|
-
|
|
15
|
-
```text
|
|
16
|
-
요청과 입력 조사
|
|
17
|
-
→ 파일 생성·편집
|
|
18
|
-
→ 구조 검증
|
|
19
|
-
→ 가능하면 시각 검증
|
|
20
|
-
→ 원래 parser로 다시 열고 내용 확인
|
|
21
|
-
→ 최종 artifact 저장
|
|
22
|
-
→ 사용자에게 실제 링크와 요약 반환
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
“작업을 마쳤어요”만 답하거나 sandbox 내부 경로만 남기면 완료가 아닙니다. 존재하지 않는 `.docx`
|
|
26
|
-
경로나 아직 생성하지 않은 artifact를 최종 링크처럼 반환하지 않습니다.
|
|
27
|
-
|
|
28
|
-
## DOCX 예
|
|
29
|
-
|
|
30
|
-
DOCX는 Microsoft Open XML ZIP 구조입니다. 단순히 `.docx` 확장자를 붙이지 않습니다.
|
|
31
|
-
|
|
32
|
-
1. 요청한 목차, 표, 이미지, 스타일을 실제로 작성합니다.
|
|
33
|
-
2. ZIP entry와 `[Content_Types].xml`, document relationship을 확인합니다.
|
|
34
|
-
3. DOCX parser로 다시 열어 문단·표·이미지 수를 확인합니다.
|
|
35
|
-
4. 모든 페이지를 렌더링해 잘림, 빈 페이지, 겹침, 깨진 한글을 확인합니다.
|
|
36
|
-
5. 수정 후 parser와 렌더링을 다시 실행합니다.
|
|
37
|
-
6. 존재하고 0바이트가 아닌 최종 `.docx`만 artifact로 반환합니다.
|
|
38
|
-
|
|
39
|
-
다른 형식도 동일합니다. PDF는 모든 페이지, spreadsheet는 수식과 셀 type, image는 실제 크기와
|
|
40
|
-
디코딩, ZIP은 entry 경로와 traversal 안전성을 확인합니다.
|
|
41
|
-
|
|
42
|
-
## Plugin이 파일을 만드는 경우
|
|
43
|
-
|
|
44
|
-
Plugin은 `file_write` 권한을 요청하고 Host가 제공하는 artifact 경계로 결과를 전달합니다.
|
|
45
|
-
`sandbox_python` 응답의 `summary`, `data`, `evidence`에 존재하지 않는 `sandbox:/...` 링크를 만들지
|
|
46
|
-
않습니다. Host로 복사된 artifact ID나 내부 파일 링크 문법을 사용하고, AI 최종 답변에는 다음을
|
|
47
|
-
포함합니다.
|
|
48
|
-
|
|
49
|
-
- 파일 이름과 형식
|
|
50
|
-
- 사용자가 요청한 결과 요약
|
|
51
|
-
- 다운로드 또는 앱 내부 미리보기 링크
|
|
52
|
-
- 검증한 항목과 남은 제한
|
|
53
|
-
|
|
54
|
-
## `.mplg` 산출물
|
|
55
|
-
|
|
56
|
-
플러그인 개발도 파일 작업입니다.
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
source 작성
|
|
60
|
-
→ validate
|
|
61
|
-
→ UI가 있으면 preview
|
|
62
|
-
→ signed build
|
|
63
|
-
→ package reopen·signature verify
|
|
64
|
-
→ 앱 설치·실행
|
|
65
|
-
→ 요청한 경로에 artifact 반환
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
source ZIP과 `.mplg`는 목적이 다릅니다. source ZIP은 편집용이고 `.mplg`는 설치용입니다. 둘을 같은
|
|
69
|
-
다운로드 이름이나 MIME으로 반환하지 않습니다.
|
|
70
|
-
|
|
71
|
-
플러그인의 기본 품질 게이트는 `validate → preview → build → verify`입니다. UI가 없는 package는
|
|
72
|
-
preview를 생략할 수 있지만 생략 이유를 결과에 기록합니다.
|
|
73
|
-
|
|
74
|
-
## 완료 체크
|
|
75
|
-
|
|
76
|
-
- 요청한 모든 파일이 실제로 존재하는가
|
|
77
|
-
- 파일 크기가 0보다 큰가
|
|
78
|
-
- 확장자와 내부 형식이 일치하는가
|
|
79
|
-
- 원래 parser로 다시 열리는가
|
|
80
|
-
- 모든 페이지/시트/entry를 확인했는가
|
|
81
|
-
- 미리보기와 다운로드가 같은 최종 artifact를 가리키는가
|
|
82
|
-
- sandbox 경로나 secret이 사용자 응답에 남지 않았는가
|
|
83
|
-
- 실패한 Tool이 있다면 복구·대체 결과와 제한을 설명했는가
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# 앱에서 플러그인 만들기
|
|
2
|
-
|
|
3
|
-
Plugin Builder는 개발 환경 없이 작은 Tool 또는 Skill을 만드는 경로입니다. Builder도 일반
|
|
4
|
-
`.mplg`를 생성하고 같은 설치·권한·삭제 흐름을 사용하지만, 안전하게 표현할 수 있는 범위를
|
|
5
|
-
의도적으로 제한합니다.
|
|
6
|
-
|
|
7
|
-
## 지원 범위
|
|
8
|
-
|
|
9
|
-
| 항목 | 지원 값 |
|
|
10
|
-
|---|---|
|
|
11
|
-
| capability kind | `tool`, `skill` |
|
|
12
|
-
| runtime | `text_stats`, `text_template` |
|
|
13
|
-
| UI 위치 | `card`, `screen`, `settings` 중 최대 3개 |
|
|
14
|
-
| 선택 기능 | Search Provider, Slash Command, background, notification |
|
|
15
|
-
| 권한 | `background`, `notifications` |
|
|
16
|
-
| credential | 최대 4개의 `api_token` 선언(현재 앱 화면은 고급 source 흐름에서 설정) |
|
|
17
|
-
| background 주기 | 15~10,080분 |
|
|
18
|
-
|
|
19
|
-
Builder는 ID, 이름, 설명, version, capability 제목과 runtime 설정을 입력받아 최소 Manifest와
|
|
20
|
-
Runtime v1 UI를 만듭니다. UI 코드를 생성하거나 외부 코드를 실행하지 않습니다.
|
|
21
|
-
|
|
22
|
-
## Builder가 적합한 예
|
|
23
|
-
|
|
24
|
-
- 입력 텍스트의 길이와 단어 수를 계산하는 Tool
|
|
25
|
-
- 정해진 템플릿으로 회의 메모를 정리하는 Skill
|
|
26
|
-
- settings의 작은 정적 목록을 Search Provider로 노출
|
|
27
|
-
- 정해진 문구를 일정 주기로 알리는 개인 알림
|
|
28
|
-
|
|
29
|
-
## 소스 프로젝트로 전환할 때
|
|
30
|
-
|
|
31
|
-
다음 중 하나가 필요하면 공식 CLI 프로젝트를 사용합니다.
|
|
32
|
-
|
|
33
|
-
- Runtime v2의 화면·내비게이션·theme·Response UI
|
|
34
|
-
- 외부 HTTPS API, OAuth, Remote MCP
|
|
35
|
-
- 여러 데이터 source와 동적 목록
|
|
36
|
-
- `sandbox_python` 데이터 처리
|
|
37
|
-
- child package와 dependency
|
|
38
|
-
- Cloud Secret, Connector retry/rate-limit
|
|
39
|
-
- package asset, 이미지, 복수 source file
|
|
40
|
-
|
|
41
|
-
[시작하기](getting-started.md)의 `@morit/cli plugin setup`으로 프로젝트를 만들고 Builder에서 정한
|
|
42
|
-
ID와 capability 이름을 유지하면 사용자가 기능을 다시 익힐 필요가 없습니다. 기존 설치를 업데이트할
|
|
43
|
-
때는 publisher와 서명 identity, plugin ID, semantic version 순서를 유지해야 합니다.
|
|
44
|
-
|
|
45
|
-
## 실제 확인
|
|
46
|
-
|
|
47
|
-
Builder 생성 성공만 확인하지 말고 다음을 앱에서 실행합니다.
|
|
48
|
-
|
|
49
|
-
1. 생성 직후 설치 목록에 나타나는지
|
|
50
|
-
2. 필요한 권한을 거부했을 때 기능이 실행되지 않는지
|
|
51
|
-
3. Tool/Skill과 Slash Command가 정확한 capability를 호출하는지
|
|
52
|
-
4. background/notification을 켜고 끌 수 있는지
|
|
53
|
-
5. 설정 변경 후 다시 열었을 때 유지되는지
|
|
54
|
-
6. 삭제 후 pending 알림과 credential이 정리되는지
|
|
55
|
-
|
|
56
|
-
고급 화면 개발은 [화면, 레이아웃, 내비게이션](screens-layout-navigation.md)부터 시작하세요.
|