@morit/cli 1.1.0 → 1.2.0

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.
@@ -0,0 +1,175 @@
1
+ # Plugin Local Storage와 AI 접근
2
+
3
+ Plugin Local Storage는 Host가 관리하는 영구 JSON 저장소입니다. 데이터는 `user_id + installation_id +
4
+ instance_id + namespace + key`로 분리되며 플러그인 코드가 파일 경로나 DB에 직접 접근하지 않습니다.
5
+ UI, Tool, Skill, Background는 같은 Instance 저장소를 사용합니다.
6
+
7
+ ## Manifest 선언
8
+
9
+ ```json
10
+ {
11
+ "schema_version": 2,
12
+ "permissions": ["storage", "ai_storage"],
13
+ "data_policy": "retain",
14
+ "storage": {
15
+ "version": 2,
16
+ "namespaces": [
17
+ {"id": "default", "max_bytes": 131072, "ai_access": "read"},
18
+ {"id": "tasks", "max_bytes": 262144, "ai_access": "read_write"}
19
+ ],
20
+ "migrations": [
21
+ {
22
+ "from": 1,
23
+ "to": 2,
24
+ "namespace": "tasks",
25
+ "rename": {"today": "today_tasks"},
26
+ "delete": ["legacy_filter"]
27
+ }
28
+ ]
29
+ }
30
+ }
31
+ ```
32
+
33
+ - `storage` 선언에는 schema 2와 `storage` 권한이 필요합니다.
34
+ - namespace는 1~16개, ID는 안전한 plugin identifier입니다.
35
+ - `max_bytes`는 1~256 KiB이고 전체 Installation은 512 KiB 이하입니다.
36
+ - namespace마다 최대 256개 key, key는 최대 80자, 값 하나는 UTF-8 JSON 32 KiB 이하입니다.
37
+ - migration 선언은 최대 64개이며 version은 1부터 1000까지 한 단계씩 증가합니다.
38
+ - 값은 문자열, 유한한 숫자, boolean, `null`, JSON 객체·목록을 사용할 수 있습니다.
39
+ - `ai_access`는 `none`, `read`, `read_write`입니다. 하나라도 AI에 공개하면 Manifest에
40
+ `ai_storage` 권한도 선언해야 합니다.
41
+
42
+ schema 1 플러그인이 기존 `storage` 권한만 요청한 경우 Host는 호환용 `default` namespace(version 1,
43
+ 128 KiB)를 제공합니다. 새 플러그인은 명시적 schema 2 선언을 사용하세요.
44
+
45
+ ## 표준 Storage Tool
46
+
47
+ Host는 저장소를 선언한 각 Instance에 다음 capability를 제공합니다.
48
+
49
+ | Tool | arguments | 동작 |
50
+ |---|---|---|
51
+ | `morit.storage.list` | `namespace`, 선택 `query`, 선택 `limit` | key와 값 검색·목록 |
52
+ | `morit.storage.get` | `namespace`, `key` | 단일 항목 조회 |
53
+ | `morit.storage.create` | `namespace`, `key`, `value` | 없는 key 생성 |
54
+ | `morit.storage.update` | `namespace`, `key`, `value`, `revision` | revision 일치 시 수정 |
55
+ | `morit.storage.delete` | `namespace`, `key`, `revision` | revision 일치 시 삭제 |
56
+
57
+ `namespace`를 생략하면 `default`, 없으면 첫 namespace를 사용합니다. `list.limit`은 1~100이고
58
+ 응답 크기 때문에 일부만 반환하면 `data.has_more`가 `true`입니다. create는 같은 key를 덮어쓰지
59
+ 않으며 update/delete는 직전에 읽은 양의 정수 `revision`을 요구합니다.
60
+
61
+ 모든 결과는 공통 `ExtensionResult` 형태입니다.
62
+
63
+ ```json
64
+ {
65
+ "summary": "task-42 항목을 수정했어요.",
66
+ "data": {
67
+ "namespace": "tasks",
68
+ "item": {
69
+ "key": "task-42",
70
+ "value": {"title": "과학 보고서", "done": true},
71
+ "revision": 3,
72
+ "created_at": "2026-08-17T01:00:00+00:00",
73
+ "updated_at": "2026-08-17T02:00:00+00:00"
74
+ }
75
+ },
76
+ "evidence": [
77
+ {"type": "plugin_storage", "instance_id": "...", "namespace": "tasks", "key": "task-42", "revision": 3}
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ## Saved Entity
83
+
84
+ AI에서 저장·즐겨찾기한 Plugin 항목은 별도 DB 사본을 만들지 않고 같은 Local Storage의
85
+ `saved_entities` namespace를 source of truth로 사용합니다. UI, Tool, Skill과 AI 표준 Storage Tool이
86
+ 같은 key·revision을 읽고 쓰므로 한 경로의 변경이 다른 경로에도 즉시 반영됩니다.
87
+
88
+ ```json
89
+ {
90
+ "type": "school.assignment",
91
+ "title": "과학 보고서",
92
+ "subtitle": "금요일까지",
93
+ "description": "실험 결과 정리",
94
+ "url": "https://school.example/assignments/42",
95
+ "image_url": "https://school.example/images/42.png",
96
+ "favorite": true,
97
+ "data": {"due": "2026-08-21"},
98
+ "source_capability": "school_life.assignments"
99
+ }
100
+ ```
101
+
102
+ `type`과 `title`은 필수입니다. 선택 필드는 `subtitle`, `description`, HTTPS `url`/`image_url`,
103
+ boolean `favorite`, bounded JSON `data`, 안전한 identifier `source_capability`뿐입니다. CRUD는
104
+ `morit.storage.list/get/create/update/delete`를 그대로 사용하며 기존 namespace 권한, quota,
105
+ revision, generation, credential 차단 검사가 동일하게 적용됩니다.
106
+
107
+ ## UI binding과 갱신
108
+
109
+ UI Runtime v2 data source에서 표준 Tool을 그대로 사용합니다.
110
+
111
+ ```json
112
+ {
113
+ "id": "saved_tasks",
114
+ "capability": "morit.storage.list",
115
+ "trigger": "load",
116
+ "query": "",
117
+ "arguments": {"namespace": "tasks", "limit": 20}
118
+ }
119
+ ```
120
+
121
+ 표시는 `data.saved_tasks.data.items`를 source로 사용하는 `list`, 단일 값은
122
+ `{{data.saved_task.data.item.value.title}}`처럼 binding합니다. UI가 Storage create/update/delete를
123
+ 완료하거나 AI 응답이 끝나면 Host가 storage revision을 갱신해 Storage data source만 다시 읽습니다.
124
+ 앱 재개 시에도 갱신합니다. 일반 Tool 결과 data source는 불필요하게 재실행하지 않습니다.
125
+ 서버 Background가 화면과 무관하게 값을 바꾸는 namespace는 source에 `refresh_seconds: 30` 이상을
126
+ 선언해 화면이 열린 동안에도 주기적으로 맞춥니다.
127
+
128
+ Tool·Skill·Background capability는 `storage` 권한을 선언하면 동일 Instance의 `plugin_storage`
129
+ snapshot을 읽고, 결과의 `data.host_actions`에 `plugin_storage_set` 또는
130
+ `plugin_storage_delete`를 반환해 같은 canonical 저장소를 원자적으로 변경할 수 있습니다.
131
+ `namespace`를 명시하면 해당 namespace를 사용합니다.
132
+
133
+ ## AI 권한과 보안 경계
134
+
135
+ 설치 또는 설정에서 사용자가 `ai_storage`를 한 번 허용하면 Morit AI는 매 CRUD마다 다시 승인받지
136
+ 않습니다. 하지만 다음 검사는 항상 적용됩니다.
137
+
138
+ - 현재 사용자에게 속하고 활성화된 Instance만 선택
139
+ - 해당 Installation·Instance·namespace와 package generation 일치
140
+ - `ai_access: read`는 list/get만, `read_write`만 mutation 허용
141
+ - 다른 사용자, 다른 Plugin/Instance, 선언하지 않은 namespace 접근 차단
142
+ - key 이름에 credential, password, secret, token, access token, refresh token, API key,
143
+ private key 계열을 저장하지 못하도록 차단
144
+ - 실행 코드·파일 경로·DB 접근은 제공하지 않음
145
+
146
+ Host가 AI에 노출하는 tool handle은 Instance와 capability를 해시한 불투명 ID입니다. AI는 임의
147
+ `instance_id`를 arguments로 넘겨 scope를 바꿀 수 없습니다.
148
+
149
+ ## 오류 구분
150
+
151
+ | code | 의미 | 처리 |
152
+ |---|---|---|
153
+ | `PLUGIN_STORAGE_PERMISSION_DENIED` | 권한·namespace·AI access 부족 | 설정과 manifest 확인 |
154
+ | `PLUGIN_STORAGE_NOT_FOUND` | key 없음 | create 여부를 사용자 의도에 따라 결정 |
155
+ | `PLUGIN_STORAGE_QUOTA_EXCEEDED` | 값·namespace·Installation 한도 초과 | 데이터를 줄이거나 정리 |
156
+ | `PLUGIN_STORAGE_CONFLICT` | 중복 create, stale revision/generation | list/get 후 최신 revision으로 재시도 |
157
+ | `PLUGIN_STORAGE_INVALID` | key, JSON, query, arguments 형식 오류 | 입력 수정 |
158
+
159
+ 오류를 not-found나 빈 목록으로 바꾸지 마세요. 특히 conflict에서 revision 검사를 제거하면 동시에
160
+ 수정한 사용자 데이터를 잃을 수 있습니다.
161
+
162
+ ## 업데이트, migration, 삭제, 초기화
163
+
164
+ - 업데이트: 같은 Installation/Instance의 데이터는 유지되고 설치 transaction 안에서 version을
165
+ 한 단계씩 올립니다. 각 `(from, namespace)` migration은 한 번만 선언하며 rename 대상 충돌 시
166
+ 업데이트 전체가 실패해 이전 package와 데이터가 유지됩니다.
167
+ - `data_policy: purge`: uninstall 때 Plugin Local Storage를 삭제합니다.
168
+ - `data_policy: retain`: uninstall 뒤 같은 사용자·Plugin Installation ID로 재설치할 때 복구합니다.
169
+ - credential, OAuth state, pending action은 retain과 무관하게 별도 보안 수명주기로 정리됩니다.
170
+ - 사용자가 설정의 **저장 데이터 초기화**를 누르면 해당 Instance 데이터만 삭제합니다. HTTP API는
171
+ `DELETE /v1/plugin-instances/{instance_id}/storage`이며 다른 Instance에는 영향이 없습니다.
172
+ - disable은 데이터를 삭제하지 않지만 읽기·쓰기를 모두 막습니다.
173
+
174
+ 배포 전에는 두 사용자와 둘 이상의 Instance로 격리, read-only AI mutation 차단, quota,
175
+ stale revision/generation, purge/retain, migration 충돌과 재시작 복원을 검증하세요.
@@ -74,6 +74,18 @@ UI에서 참조하는 package 이미지는 최대 512 KiB이고 가로·세로
74
74
  계산한 frame 합산 pixel은 최대 16,000,000입니다. 확장자·MIME에 대응하는 magic byte와 실제 이미지
75
75
  포맷도 일치해야 합니다.
76
76
 
77
+ ### 웹 프로젝트에서 아이콘 올리기
78
+
79
+ developers.moring.co의 **플러그인 프로젝트 관리 → 정보 → 앱 아이콘**에서는 PNG, JPEG, WebP
80
+ 파일을 직접 선택하거나 끌어 놓을 수 있습니다. 업로드가 완료되면 이미지는
81
+ `assets/plugin-icon.*`에 저장되고 `manifest.icon`도 같은 revision에서 함께 갱신됩니다. 기존에 이
82
+ 화면에서 올린 아이콘을 교체하거나 제거할 때도 파일과 manifest가 원자적으로 함께 반영됩니다.
83
+
84
+ 아이콘은 512 KiB 이하, 가로·세로 각각 4096 px 이하, 전체 16,000,000 pixel 이하만 허용됩니다.
85
+ 확장자만 바꾼 파일이나 실제 포맷이 다른 이미지는 서버 검증에서 거부됩니다. MCP가 바이너리 파일을
86
+ 직접 전송하지 못하더라도 이 화면에서 한 번 올린 아이콘은 프로젝트 source에 포함되므로 이후 MCP,
87
+ CLI pull·build에서도 원래 이미지 바이트와 `manifest.icon` 경로가 그대로 유지됩니다.
88
+
77
89
  ## 파일별 책임
78
90
 
79
91
  - `manifest.json`: 패키지 신원, 버전, 전역 권한, 연결과 데이터 정책
@@ -32,7 +32,7 @@ claude mcp add --transport http --scope user \
32
32
 
33
33
  | Tool | 목적 |
34
34
  |---|---|
35
- | `morit_sdk_contract` | 현재 SDK/Host capability contract 전체 반환 |
35
+ | `morit_sdk_contract` | SDK 1.7.8 Theme·Storage·UI/Host capability contract 전체 반환 |
36
36
  | `morit_docs_search` | bundled 공식 문서 검색 |
37
37
  | `morit_docs_get` | 검색 결과의 Markdown 한 파일 읽기 |
38
38
 
@@ -53,7 +53,7 @@ claude mcp add --transport http --scope user \
53
53
  | Tool | 목적 |
54
54
  |---|---|
55
55
  | `morit_project_validate` | 현재 revision의 source 계약 검사 |
56
- | `morit_project_preview` | private HTML preview artifact 생성 |
56
+ | `morit_project_preview` | light/dark 토글을 포함한 script-free private HTML preview 생성 |
57
57
  | `morit_project_source_download` | 편집 가능한 source ZIP 생성 |
58
58
  | `morit_build_start` | immutable build/Deployment 시작 |
59
59
  | `morit_build_status` | build state와 package artifact 조회 |
@@ -108,9 +108,15 @@ AI가 UUID나 경로를 추측하지 않고 list/create/get 결과를 그대로
108
108
  `null`은 파일 삭제입니다. revision이 오래되면 전체 요청을 거부하고 최신 source를 반환하므로
109
109
  `files_get` → 변경 병합 → `files_put`을 반복합니다. conflict를 `force`로 숨기지 않습니다.
110
110
 
111
- 이미지 asset과 child `.mplg`는 검증된 binary envelope로만 JSON 경계를 지나며 최종 source ZIP과
111
+ 이미지 asset과 child `.mplg`는 `morit-base64-v1:<canonical-base64>` binary envelope로만 JSON
112
+ 경계를 지나며 MIME magic과 경로를 검증합니다. 최종 source ZIP과
112
113
  package에서는 원래 바이트로 복원됩니다.
113
114
 
115
+ Theme/Storage를 생성·수정할 때는 contract의 `ui_runtime.theme_*`, `storage_tools`,
116
+ `storage_ai_access`, `storage_limits`를 기준으로 Manifest와 UI fragment를 함께 변경합니다. Remote
117
+ MCP validation은 Host와 같은 잘못된 token, 확정적으로 보이지 않는 색 조합, namespace/permission,
118
+ migration과 binding 오류를 거부하고 정적으로 확정하기 어려운 대비는 warning으로 반환합니다.
119
+
114
120
  ## Build와 다운로드
115
121
 
116
122
  ```text
@@ -1,6 +1,6 @@
1
1
  # 학교 생활 플러그인 개인정보 처리 안내
2
2
 
3
- 최종 수정일: 2026-08-13
3
+ 최종 수정일: 2026-08-17
4
4
 
5
5
  학교 생활 플러그인은 학생이 선택한 학교, 학년, 반을 기준으로 시간표·급식·학사일정을
6
6
  보여 주고 기기 안에서 알림과 아침 브리핑을 구성합니다. 플러그인 설치 전에 아래 데이터
@@ -11,6 +11,7 @@
11
11
  - 사용자가 직접 선택한 학교 코드, 교육청 코드, 학년, 반
12
12
  - 알레르기 필터, 알림 사용 여부와 알림 시간
13
13
  - 중복 알림을 막기 위한 마지막 실행 날짜
14
+ - 사용자가 Morit AI에 추가·수정·완료·삭제를 요청한 학교 할 일
14
15
  - 교육부 NEIS 공개 API가 제공하는 시간표, 급식, 학사일정
15
16
 
16
17
  이 플러그인은 이름, 주민등록번호, 학생 번호, 성적, 연락처, 계정 비밀번호를 요청하거나
@@ -22,6 +23,10 @@
22
23
  사용자의 Plugin Instance 저장소에만 보관합니다. 조회한 공개 학교 정보는 화면 표시와
23
24
  알림 생성에만 사용합니다. 다른 사용자나 다른 Instance와 공유하지 않습니다.
24
25
 
26
+ 학교 할 일은 `tasks` namespace에 저장됩니다. 사용자가 설치/설정에서 `ai_storage` 권한을 허용하면
27
+ Morit AI가 이 namespace를 별도 매 작업 승인 없이 읽고 변경할 수 있습니다. Host는 현재 사용자와
28
+ 활성 Instance를 고정하고 다른 namespace·사용자·플러그인 접근을 차단합니다.
29
+
25
30
  외부 요청은 선언된 NEIS HTTPS endpoint로만 제한되며 Morit의 SSRF filtering proxy를
26
31
  통과합니다. 플러그인 코드에는 운영 credential이나 사용자 token이 포함되지 않습니다.
27
32
 
@@ -36,6 +41,7 @@ Manifest의 data policy는 `purge`입니다. 사용자가 Plugin Instance 또는
36
41
 
37
42
  - 네트워크: NEIS 공개 학교 정보 조회
38
43
  - 저장소: 학교·학년·반과 사용자 설정 저장
44
+ - AI 저장소: 사용자가 허용한 경우 학교 할 일 목록의 조회·추가·수정·삭제
39
45
  - 알림: 사용자가 켠 경우 다음 학교 일정과 아침 브리핑 알림 예약
40
46
  - 백그라운드: 사용자가 켠 경우 공개 학교 정보 갱신과 브리핑 준비
41
47
 
@@ -7,7 +7,8 @@ Morit은 두 MCP 연결을 제공합니다.
7
7
  | Local `@morit/plugin-mcp` | 개발자 PC의 stdio process | 로컬 파일 직접 편집, 빠른 preview/build |
8
8
  | Remote `https://morit-api.moring.co/mcp` | Morit Cloud Streamable HTTP | 파일시스템 없는 AI client, 조직 Project·Secret·Deployment |
9
9
 
10
- 두 연결은 `@morit/cli`와 Host 같은 Plugin contract를 사용합니다. Local source에 접근할 필요가
10
+ 두 연결은 SDK 1.7.8의 `@morit/cli`와 Host Plugin contract를 사용합니다. 계약에는 mode별 Plugin
11
+ Theme, Local Storage/AI access, 표준 Storage Tool, UI binding과 safe preview가 포함됩니다. Local source에 접근할 필요가
11
12
  없으면 Remote를, 현재 저장소 파일을 직접 고쳐야 하면 Local을 선택합니다.
12
13
 
13
14
  ## 준비
@@ -114,7 +115,7 @@ morit_sdk_contract
114
115
  → morit_project_files_get
115
116
  → morit_project_files_put
116
117
  → morit_project_validate
117
- → UI가 있으면 morit_project_preview
118
+ → UI가 있으면 morit_project_preview(light/dark 확인)
118
119
  → morit_build_start / morit_build_status
119
120
  → morit_artifact_download
120
121
  ```
@@ -137,6 +138,16 @@ morit_sdk_contract
137
138
  MCP가 반환한 project ID, revision, artifact ID를 다음 호출에 그대로 사용합니다. 경로를 추측하거나
138
139
  build 전에 완료했다고 보고하지 않습니다.
139
140
 
141
+ PNG/JPEG/GIF/WebP icon·UI asset과 `children/*.mplg`는
142
+ `morit-base64-v1:<canonical-base64>` 문자열로 `files_put`할 수 있습니다. text 경로에 envelope를
143
+ 쓰거나 이미지 확장자와 magic byte가 다르면 Local/Remote MCP가 거부합니다. `files_get`도 같은
144
+ envelope를 반환하므로 수정하지 않은 바이트는 그대로 다음 revision에 유지합니다.
145
+
146
+ Storage 기능을 생성할 때는 먼저 `morit_sdk_contract`의 `permissions`, `storage_tools`,
147
+ `storage_ai_access`, `storage_limits`를 읽고 Manifest `storage`와 UI data source를 함께 작성합니다.
148
+ AI가 쓰는 namespace는 `ai_access: read_write`와 `ai_storage` 권한을 모두 선언합니다. MCP는 package
149
+ runtime 데이터를 읽는 우회 경로가 아니며 개발 source와 artifact만 다룹니다.
150
+
140
151
  ## Remote MCP 작업 순서
141
152
 
142
153
  ```text
@@ -74,7 +74,7 @@ GET과 POST만 지원하며 redirect를 따라가지 않습니다. endpoint는
74
74
 
75
75
  ### Remote MCP
76
76
 
77
- Morit Host 1.7.6의 plugin outbound MCP adapter는 `mcp_http`입니다. 개발자용 Local/Remote Plugin
77
+ Morit Host 1.7.8의 plugin outbound MCP adapter는 `mcp_http`입니다. 개발자용 Local/Remote Plugin
78
78
  MCP 서버와 이름이 비슷하지만, 설치된 플러그인이 선언한 외부 MCP Tool을 Host 경계에서 호출하는
79
79
  runtime입니다.
80
80
 
@@ -120,6 +120,11 @@ Archive API process에서 실행하지 않고 별도 rootless Docker + gVisor wo
120
120
  AI는 enabled 상태이고 사용자에게 허용된 capability만 발견합니다. 제목과 설명은 모델이 언제 이
121
121
  기능을 선택할지 판단할 수 있게 동작과 입력을 구체적으로 씁니다.
122
122
 
123
+ Slash Command나 UI extension은 선택적 진입점일 뿐 discovery 조건이 아닙니다. 활성 Plugin의 모든
124
+ `tool`·`skill`은 매 AI turn의 actor-scoped capability catalog에 자동 포함됩니다. 설치·업데이트·
125
+ enable/disable·권한 변경·Connection 생성/수정/삭제·OAuth 완료·삭제 뒤 Host는 Instance route cache를
126
+ 즉시 비워 다음 turn과 실행이 새 generation을 사용하게 합니다.
127
+
123
128
  ```json
124
129
  {
125
130
  "id": "school_life.today",
@@ -160,4 +165,8 @@ retry는 Connector에 선언한 bounded 정책만 사용합니다. Tool 하나
160
165
  즉시 포기하지 말고, 오케스트레이터가 대체 Tool·재연결·부분 결과를 선택할 수 있도록 공개 오류를
161
166
  구체적으로 유지합니다.
162
167
 
168
+ 실행 준비 상태의 대표 code는 `EXTENSION_NOT_FOUND`, `EXTENSION_PERMISSION_DENIED`,
169
+ `EXTENSION_CONNECTION_REQUIRED`, `EXTENSION_DISABLED`, `EXTENSION_VERSION_MISMATCH`,
170
+ `EXTENSION_DEPENDENCY_REQUIRED`입니다. 빈 Tool 목록이나 일반 실패 문구로 합치지 마세요.
171
+
163
172
  권한과 알림은 [권한, 설정, 저장소, 알림](permissions-and-data.md)을 참고하세요.
@@ -97,6 +97,47 @@ child입니다.
97
97
  `aspect_ratio` 0.1~20, `flex` 1~24를 사용합니다. `padding`과 `margin`의 정확한 입력 형태와
98
98
  theme 색상 역할은 [토큰과 반응형](design-tokens-responsive.md)에 있습니다.
99
99
 
100
+ ## Plugin Theme
101
+
102
+ theme를 생략한 기존 플러그인은 Morit의 현재 Material 3 라이트·다크 theme를 그대로 상속합니다.
103
+ 선택적으로 base와 mode별 일부 값만 재정의할 수 있고 누락 값은 항상 Host 값으로 채웁니다.
104
+
105
+ ```json
106
+ {
107
+ "theme": {
108
+ "radius": 18,
109
+ "typography": {"scale": 1.0, "body_weight": 400, "title_weight": 700},
110
+ "surface": {"elevation": 1},
111
+ "border": {"width": 1, "radius": 18},
112
+ "icon": {"size": 22},
113
+ "states": {"disabled_opacity": 0.38},
114
+ "light": {
115
+ "color_scheme": {
116
+ "primary": "#315DA8",
117
+ "on_primary": "#FFFFFF",
118
+ "surface": "#F9F9FF",
119
+ "on_surface": "#1A1B20"
120
+ }
121
+ },
122
+ "dark": {
123
+ "color_scheme": {
124
+ "primary": "#AFC6FF",
125
+ "on_primary": "#002F66",
126
+ "surface": "#111318",
127
+ "on_surface": "#E2E2E9"
128
+ },
129
+ "states": {"selected_color": "primary_container"}
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ 지원 그룹은 `color_scheme`, `typography`, `surface`, `border`, `icon`, `states`, `radius`,
136
+ `spacing`, `density`와 최상위 `light`, `dark`입니다. mode variant 안에 다시 `light`/`dark`를 중첩할
137
+ 수 없습니다. 명백히 같은 literal 전경/배경은 validate 오류이고, 계산 가능한 대비가 4.5:1보다
138
+ 낮으면 경고합니다. semantic token이나 Host 상속 때문에 정적으로 확정할 수 없는 조합은 Preview와
139
+ 실제 앱에서 확인합니다.
140
+
100
141
  ## State
101
142
 
102
143
  `initial_state`가 허용 state key와 초기값을 선언합니다. key는 영문자로 시작하고 영문·숫자·밑줄을
@@ -144,6 +185,24 @@ arguments에 전달하고, 다른 글자와 섞으면 텍스트 template이 됩
144
185
  {"label": "{{state.period}} 일정"}
145
186
  ```
146
187
 
188
+ ### Local Storage binding
189
+
190
+ 저장소도 같은 data source/action 흐름을 사용합니다.
191
+
192
+ ```json
193
+ {
194
+ "id": "tasks",
195
+ "capability": "morit.storage.list",
196
+ "trigger": "load",
197
+ "query": "",
198
+ "arguments": {"namespace": "tasks", "limit": 20}
199
+ }
200
+ ```
201
+
202
+ `list.source`에는 `data.tasks.data.items`를 사용합니다. Storage mutation이나 AI 응답 완료 뒤 Host가
203
+ Storage source만 갱신하며 앱 재개 시에도 최신 값을 읽습니다. CRUD, 권한과 migration은
204
+ [Plugin Local Storage와 AI 접근](plugin-storage.md)을 참고하세요.
205
+
147
206
  ### Capability 결과 이미지
148
207
 
149
208
  `image`와 `avatar`의 `url`은 literal URL이 아니라 capability 결과를 가리키는 전체
@@ -271,3 +330,11 @@ Host는 source별 실행을 격리하고 중복 실행을 합칩니다. 화면
271
330
  `point: "response"`도 같은 `theme`, `app_bar`, `navigation`, data/state/node/action 계약을
272
331
  재사용합니다. 단, 결과는 capability를 실행한 AI 메시지의 한 response container 안에 렌더링됩니다.
273
332
  표현 세부는 [Response UI와 Agent Timeline](ai-response-and-timeline.md)을 참고하세요.
333
+
334
+ ## Preview와 실제 Host
335
+
336
+ `morit plugin preview . --output ./dist/preview.html` 또는 `morit_project_preview`는 validate를 먼저
337
+ 통과한 config를 실행 코드 없이 안전한 HTML로 렌더링합니다. 상단 토글로 light/dark variant와 Host
338
+ 상속 결과를 비교할 수 있습니다. 같은 manifest를 Flutter 앱이 실제 Material widget으로 렌더링하므로
339
+ node, binding, theme 계약은 공유하지만 focus, 키보드, route stack, 권한, 실제 Tool 호출은 서명
340
+ package를 앱에 설치해 최종 확인합니다.
@@ -10,16 +10,16 @@ SHA-256을 함께 남기면 나중에 같은 결과를 재현할 수 있습니
10
10
 
11
11
  | 패키지 | 크기 | SHA-256 |
12
12
  |---|---:|---|
13
- | `school-life-1.4.0.mplg` | 262758 bytes | `f246d3abff4eac85e1a991d8f436c5381b394858843c95d6d4d88620a1b760b7` |
13
+ | `school-life-1.5.0.mplg` | 263184 bytes | `3b6397ca1ef26833fe842087b0ecf5f05621bd45f9d80b8a422b6547c060f603` |
14
14
  | `notion-1.1.0.mplg` | 3044 bytes | `eba57c440b305343c5cf917259e30e50b322272cfadb0ed64d39ecfc47539c4e` |
15
15
 
16
16
  ```powershell
17
17
  python tools/morit_plugin.py verify `
18
- examples/plugins/school-life/dist/school-life-1.4.0.mplg
18
+ examples/plugins/school-life/dist/school-life-1.5.0.mplg
19
19
  python tools/morit_plugin.py verify `
20
20
  examples/plugins/notion/dist/notion-1.1.0.mplg
21
21
  Get-FileHash -Algorithm SHA256 @(
22
- 'examples/plugins/school-life/dist/school-life-1.4.0.mplg',
22
+ 'examples/plugins/school-life/dist/school-life-1.5.0.mplg',
23
23
  'examples/plugins/notion/dist/notion-1.1.0.mplg'
24
24
  )
25
25
  ```
@@ -59,7 +59,7 @@ Slash·Search·Tool routing, UI/Response 계약을 검사합니다. transport fi
59
59
  ```powershell
60
60
  $env:MORIT_E2E_ENV = 'C:\secure\morit-backend.env'
61
61
  $env:MORIT_E2E_API = 'https://morit-api.moring.co'
62
- $env:MORIT_SCHOOL_PLUGIN = 'examples/plugins/school-life/dist/school-life-1.4.0.mplg'
62
+ $env:MORIT_SCHOOL_PLUGIN = 'examples/plugins/school-life/dist/school-life-1.5.0.mplg'
63
63
  $env:MORIT_NOTION_PLUGIN = 'examples/plugins/notion/dist/notion-1.1.0.mplg'
64
64
 
65
65
  # 실제 Notion 승인을 할 때만 0보다 큰 값을 사용합니다.
@@ -1,5 +1,5 @@
1
1
  {
2
- "sdk_version": "1.7.6",
2
+ "sdk_version": "1.7.8",
3
3
  "manifest_schema_versions": [1, 2],
4
4
  "recommended_schema_version": 2,
5
5
  "package_content_type": "application/vnd.morit.plugin+zip",
@@ -30,7 +30,8 @@
30
30
  "slash_commands",
31
31
  "connectors",
32
32
  "dependencies",
33
- "data_policy"
33
+ "data_policy",
34
+ "storage"
34
35
  ],
35
36
  "fragment_directories": {
36
37
  "capabilities": {"target": "capabilities", "kind": null},
@@ -51,7 +52,10 @@
51
52
  "connector": ["id", "kind", "label", "description", "credential_id", "cloud_connection_id", "cloud_secret_id", "endpoint", "timeout_seconds", "retry", "rate_limit"],
52
53
  "developer": ["name", "url"],
53
54
  "required_secret": ["id", "label", "description", "required"],
54
- "dependency": ["id", "plugin_id", "required", "min_version", "max_version", "package_path", "exposed_capabilities"]
55
+ "dependency": ["id", "plugin_id", "required", "min_version", "max_version", "package_path", "exposed_capabilities"],
56
+ "storage": ["version", "namespaces", "migrations"],
57
+ "storage_namespace": ["id", "max_bytes", "ai_access"],
58
+ "storage_migration": ["from", "to", "namespace", "rename", "delete"]
55
59
  },
56
60
  "capability_runtime_shorthand": ["adapter", "entrypoint", "parameters"],
57
61
  "capability_kinds": ["tool", "skill", "provider", "background", "notification"],
@@ -68,8 +72,63 @@
68
72
  "background",
69
73
  "account_read",
70
74
  "storage",
75
+ "ai_storage",
71
76
  "credentials"
72
77
  ],
78
+ "storage_tools": [
79
+ "morit.storage.list",
80
+ "morit.storage.get",
81
+ "morit.storage.create",
82
+ "morit.storage.update",
83
+ "morit.storage.delete"
84
+ ],
85
+ "storage_ai_access": ["none", "read", "read_write"],
86
+ "saved_entity": {
87
+ "namespace": "saved_entities",
88
+ "tools": [
89
+ "morit.storage.list",
90
+ "morit.storage.get",
91
+ "morit.storage.create",
92
+ "morit.storage.update",
93
+ "morit.storage.delete"
94
+ ],
95
+ "required_value_fields": ["type", "title"],
96
+ "optional_value_fields": [
97
+ "subtitle",
98
+ "description",
99
+ "url",
100
+ "image_url",
101
+ "favorite",
102
+ "data",
103
+ "source_capability"
104
+ ]
105
+ },
106
+ "notification_actions": {
107
+ "types": ["check", "capability", "snooze", "open_plugin", "reply"],
108
+ "max_per_notification": 4,
109
+ "capability_types": ["check", "capability", "reply"],
110
+ "snooze_minutes": {"minimum": 1, "maximum": 1440},
111
+ "dedupe": "event_token_and_action_id",
112
+ "permission_revalidation": true
113
+ },
114
+ "ai_response": {
115
+ "state_field": "data.__morit_response.state",
116
+ "states": ["loading", "partial", "completed", "failed"],
117
+ "retryable_field": "data.__morit_response.retryable",
118
+ "fallback_nodes": ["metric", "progress", "list", "timeline", "calendar", "chart", "table", "empty"],
119
+ "image_artifact_mime_types": ["image/png", "image/jpeg", "image/gif", "image/webp"],
120
+ "max_inline_image_bytes": 25165824,
121
+ "max_inline_image_dimension": 4096,
122
+ "max_inline_image_pixels": 16777216
123
+ },
124
+ "storage_limits": {
125
+ "namespaces": 16,
126
+ "keys_per_namespace": 256,
127
+ "value_bytes": 32768,
128
+ "namespace_bytes": 262144,
129
+ "installation_bytes": 524288,
130
+ "migrations": 64
131
+ },
73
132
  "runtime_adapters": [
74
133
  "text_stats",
75
134
  "text_template",
@@ -112,6 +171,7 @@
112
171
  "timeline",
113
172
  "calendar",
114
173
  "chart",
174
+ "table",
115
175
  "form",
116
176
  "dialog",
117
177
  "sheet",
@@ -209,10 +269,17 @@
209
269
  "app_bar_fields": ["title", "subtitle", "center_title", "pinned", "leading", "actions"],
210
270
  "navigation_fields": ["type", "items", "selected_state_key", "persist", "label_behavior", "rail_breakpoint"],
211
271
  "menu_item_fields": ["id", "label", "icon", "selected_icon", "value", "show_as", "action"],
212
- "theme_fields": ["color_scheme", "radius", "spacing", "density"],
272
+ "theme_fields": ["color_scheme", "typography", "surface", "border", "icon", "states", "radius", "spacing", "density", "light", "dark"],
273
+ "theme_variant_fields": ["color_scheme", "typography", "surface", "border", "icon", "states", "radius", "spacing", "density"],
274
+ "theme_typography_fields": ["scale", "body_weight", "title_weight"],
275
+ "theme_surface_fields": ["color", "container_color", "elevation"],
276
+ "theme_border_fields": ["color", "width", "radius"],
277
+ "theme_icon_fields": ["color", "size"],
278
+ "theme_state_fields": ["disabled_opacity", "selected_color", "focus_color"],
213
279
  "action_fields": ["type", "capability", "query", "arguments", "store", "target", "source", "values", "persist", "transition", "replace"],
214
280
  "actions": ["invoke", "navigate", "set_state", "refresh", "back"],
215
281
  "navigation_transitions": ["platform", "fade", "slide", "none"],
282
+ "chart_types": ["bar", "line", "donut", "scatter"],
216
283
  "navigation_types": ["tabs", "bar", "rail", "drawer", "adaptive"],
217
284
  "icon_tokens": [
218
285
  "add",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morit/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official Morit Developer CLI for Cloud Projects, validation, builds, and deployments",
5
5
  "type": "module",
6
6
  "bin": {