@illli-studio/dsh-mory 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc.
5
+
6
+ This project is licensed under the GNU Affero General Public License, version
7
+ 3 or any later version (AGPL-3.0-or-later). The complete license text is
8
+ available at:
9
+
10
+ https://www.gnu.org/licenses/agpl-3.0.html
11
+
12
+ SPDX-License-Identifier: AGPL-3.0-or-later
13
+
14
+ Source: https://github.com/illli-studio/mory
15
+ This plugin follows the Mory repository's community license. A separate
16
+ commercial license is available from the project maintainers for proprietary
17
+ distribution; see LICENSE-COMMERCIAL.md in the source repository.
package/README.md CHANGED
@@ -1,3 +1,255 @@
1
- # Temporary Holding Version
1
+ # Mory for DeepSeek Harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在 DSH 左侧栏使用 Mory,并让智能体访问同一个本地记忆库。
4
+
5
+ This package adds a native React Mory panel to the DeepSeek Harness sidebar and a
6
+ `mory_memory` agent tool. It connects to the existing Mory API; it does not embed
7
+ an iframe or start another web server.
8
+
9
+ ## Requirements / 使用条件
10
+
11
+ - DSH `>=0.2.1-rc.5 <0.3.0` with its Web UI, Connection and Tools services.
12
+ - Node.js 22.18+ for the Mory runtime and development checks.
13
+ - A running Mory service on the computer hosting DSH. The online browser-only
14
+ repository at `me.mory.cloud` is not automatically connected or imported.
15
+
16
+ ```sh
17
+ npm install -g @illli-studio/mory
18
+ mory init
19
+ mory start --background
20
+ ```
21
+
22
+ If Mory is already running, keep using it. The plugin reads `MORY_HOME/config.json`
23
+ or `~/.mory/config.json` on the DSH host, respecting its port and token. Memory
24
+ content stays in the existing Mory SQLite repository; disabling this plugin does
25
+ not delete it.
26
+
27
+ ## Install / 安装
28
+
29
+ For a local checkout, open DSH → Plugins → install from a package/directory, and
30
+ select the **absolute path to `packages/dsh-mory`**. Enable the bundle, then click
31
+ **Mory** in the left sidebar. The plugin manager handles profile installation;
32
+ do not manually edit DSH's profile manifest.
33
+
34
+ After this package has been published to npm, other users can install
35
+ `@illli-studio/dsh-mory` in DSH's plugin manager. This repository change alone
36
+ does not publish the package or add it to a third-party plugin marketplace.
37
+
38
+ On a first install, DSH may apply the plugin live. An update to an already loaded
39
+ plugin module requires restarting DSH, then refreshing the existing page. In
40
+ particular, upgrading an earlier release to 0.1.2 requires that restart to load
41
+ the settings-enabled client and Host module. Follow any `restart-required` result reported by
42
+ the plugin manager.
43
+
44
+ ## Panel / 面板
45
+
46
+ - Browse recent memories with pagination; search text and filter by project ID.
47
+ - Create a memory with title, exact text, kind, tags and a privacy flag.
48
+ - Read full content, edit, or confirm deletion inline.
49
+ - Updates and deletions require the version last read. A concurrent web/agent edit
50
+ produces a conflict and preserves your draft; cancel and reopen to reread it.
51
+ - The native panel reuses the original Mory website's branding, navigation, memory
52
+ list, controls and dialogs. All 25 original themes stay scoped to the Mory panel.
53
+ English and Chinese are included.
54
+ - Responsive width breakpoints follow the space available to the plugin, so the
55
+ Mory layout adapts correctly beside the DSH sidebar.
56
+ - Existing records above 200000 content characters or 50 tags are readable, but
57
+ use the Mory web console to edit them. The panel never saves a truncated draft.
58
+ - Type navigation and table rows follow the website layout. Filter by source,
59
+ time, visibility, tag, or project; choose 10, 20, or 50 rows per page.
60
+ - The panel loads up to 10000 memory previews for navigation counts and filters;
61
+ enter a project ID to narrow larger repositories. Search shows up to 50 ranked
62
+ matches. Clear the search to return to the complete loaded list.
63
+ - New memories, details and edits open on demand in native dialogs, with keyboard
64
+ focus handling and draft protection.
65
+
66
+ The privacy flag affects presentation and does not encrypt or restrict agent
67
+ access. Project IDs are filters, not access boundaries. Omitting a project filter
68
+ searches/lists all projects; operations by memory ID use that ID directly.
69
+
70
+ ## Settings / 设置
71
+
72
+ Open the gear button in the top-right corner, or Settings at the foot of the
73
+ Mory type navigation. The top-right entry is also available at narrow widths.
74
+
75
+ - **Memory Sync / 记忆同步**: configure a private GitHub or Gitee repository,
76
+ access token, branch, JSON snapshot path, and device name. Check the connection,
77
+ merge and push, push, or pull. Push/merge read and merge both sides before
78
+ uploading; pull preserves unrelated local records. Tombstones take precedence,
79
+ and divergent content is retained under a conflict ID. Device names apply to
80
+ memories subsequently created through this plugin or its agent tool.
81
+ - **Language / 语言**: follow DSH, English, or Chinese for Mory only.
82
+ - **Appearance / 外观主题**: select any of the 25 original website themes.
83
+ - **Health / 健康状态**: check the protected Mory API, service address, repository
84
+ count and last synchronization status.
85
+ - **Mory Connection / Mory 连接**: enter an API origin and token, check it, or
86
+ return to automatic local detection. These settings work even before a Mory
87
+ service has been connected. Advanced plugin/environment values take priority.
88
+
89
+ Preferences and credentials are persisted on the DSH host in
90
+ `MORY_HOME/dsh-plugin.json` (default `~/.mory/dsh-plugin.json`), separately from the
91
+ Mory runtime configuration. Blank token fields preserve stored credentials. Reads
92
+ return only credential-presence flags; stored token values are not sent back to
93
+ the page or the model. The file is not an encrypted vault; its access is governed
94
+ by the host filesystem. Settings updates require the revision from the last read.
95
+
96
+ Automatic backup is opt-in and runs five seconds after a successful memory
97
+ mutation through this plugin or its agent tool. Saving settings, opening a page,
98
+ or reading memories does not upload anything. Changes made directly through
99
+ another web/API client do not trigger this plugin's automatic backup. Background
100
+ errors are available in the last sync status. Synchronization includes private
101
+ memories and deletion records, so a private repository is required.
102
+
103
+ The matching `mory_settings` tool exposes `get`, `update`, `check`, `health` and
104
+ `sync`. `update` requires `expectedRevision`; `check` requires `target` (`mory` or
105
+ `sync`); `sync` requires `mode` (`pull`, `push`, or `merge`). `connection` and `sync`
106
+ objects accept an optional token: omitted/empty keeps it, `null` removes the saved
107
+ override. A changed API origin cannot inherit a token bound to another origin.
108
+ Changing providers likewise requires a token for the selected provider.
109
+
110
+ Remote commit and local reconciliation are reported separately. If the remote
111
+ commit succeeds but the local generation has changed, the result is explicitly
112
+ `status: "partial"`; the UI shows a warning and preserves local data. Pull or
113
+ merge again to reconcile. No live remote repository is written during tests.
114
+
115
+ ## Agent tool / 智能体工具
116
+
117
+ `mory_memory` exposes these actions:
118
+
119
+ | Action | Input | Result |
120
+ | --- | --- | --- |
121
+ | `status` | none | Authenticated connection check |
122
+ | `list` | optional `projectId`, `limit`, `offset` | Bounded previews and `nextOffset` |
123
+ | `search` | `query`, optional `projectId`, `limit` | Ranked memory previews |
124
+ | `context` | same as search | Relevant context, capped at 24000 characters |
125
+ | `get` | `memoryId`, optional `textOffset`, `textLimit` | Memory preview, content slice and `nextTextOffset` |
126
+ | `create` | `text` or `title`, optional `kind`, `tags`, `projectId`, `isPrivate` | Stored memory ID/revision; exact text, no extraction |
127
+ | `update` | `memoryId`, `expectedRevision`, changed fields | Corrected memory ID/revision |
128
+ | `forget` | `memoryId`, `expectedRevision` | Tombstone deletion |
129
+
130
+ List/search limit is 1–50. Tool `get` defaults to 12000 content characters per
131
+ response, maximum 20000; continue at `nextTextOffset`. Previews shorten titles,
132
+ text and tags. Panel edits read the original complete record and preserve fields
133
+ outside the editor, including the original URL and metadata.
134
+
135
+ The panel and tool call the same `MoryService.execute` operations. Native tool
136
+ name `mory_memory` avoids colliding with the existing Mory MCP tool names. You do
137
+ not need an additional MCP connection for this plugin. It does not automatically
138
+ collect conversations or inject every memory into every model request.
139
+
140
+ Example requests:
141
+
142
+ ```json
143
+ {"action":"search","query":"Mory 项目决定","limit":5}
144
+ ```
145
+
146
+ ```json
147
+ {"action":"create","text":"Mory 的 DSH 插件使用原生侧栏页面。","kind":"decision","projectId":"mory"}
148
+ ```
149
+
150
+ ```json
151
+ {"action":"update","memoryId":"<id from get>","expectedRevision":1,"text":"Corrected text"}
152
+ ```
153
+
154
+ ## Connection configuration / 连接配置
155
+
156
+ Environment variables are read on the DSH host, not the browser:
157
+
158
+ - `MORY_HOME`: directory containing `config.json` (default `~/.mory`).
159
+ - `MORY_API_URL`: override the API origin, for example `http://127.0.0.1:8787`.
160
+ - `MORY_API_TOKEN`: override the bearer token.
161
+
162
+ Advanced Loader row config accepts `apiUrl`, `configPath` (absolute path), and
163
+ `tokenEnv` (environment variable name). Credentials entered in the settings
164
+ password field are sent to the authenticated DSH backend and stored there;
165
+ reading settings does not return them. Default row config is `{}`. To override it in your own bundle patch:
166
+
167
+ ```yaml
168
+ - id: mory
169
+ config:
170
+ apiUrl: 'https://memory.example.com'
171
+ tokenEnv: MORY_REMOTE_TOKEN
172
+ ```
173
+
174
+ Use HTTPS for a remote service. URLs must be origins without path/query/embedded
175
+ credentials. The plugin does not forward a local token to a remote origin unless
176
+ an explicit token is entered for that origin or supplied through the environment,
177
+ and refuses HTTP redirects.
178
+ Remote hosting must independently provide a compatible authenticated Mory API.
179
+
180
+ `/api/mory` is an authenticated, exact POST Fetch route registered through DSH
181
+ Connection. Stored Mory tokens are never included in settings responses. Requests are limited to
182
+ 1 MiB, upstream responses to 8 MiB, and upstream calls to 40 seconds. Token
183
+ configuration errors and revision conflicts are displayed in the panel.
184
+
185
+ ## Develop, check, pack / 开发与打包
186
+
187
+ No bundler, runtime dependencies or installation scripts are required. The client
188
+ uses DSH's React module table and supported slot/services APIs. Edit
189
+ `src/client.js`, then run `build` to generate `client.js` and check JavaScript
190
+ syntax. Do not edit the generated artifact directly.
191
+
192
+ The zero-dependency generator reads the complete original website stylesheet
193
+ from `apps/web/src/styles.css` and logo from
194
+ `apps/web/public/mory-logo-premium.svg`. It preserves the stylesheet's historical
195
+ cascade, removes the Tailwind import, localizes document-root selectors inside
196
+ native `@scope (.mory-app)`, and converts width-only media rules to named Mory
197
+ container queries. Mixed orientation/media rules remain intact. The Tailwind
198
+ semantic-variable block becomes local CSS variables; no Tailwind utilities are
199
+ compiled. The panel uses native components rather than an iframe, and does not
200
+ change the DSH document root or body.
201
+
202
+ Each author build refreshes the original CSS/logo snapshots under `assets/` and
203
+ copies the original website logo to `icon.svg`. Those snapshots, source code,
204
+ build script and adapter tests ship in the npm archive, allowing `npm run build`
205
+ and `npm pack` to work from an extracted package without the rest of this
206
+ repository. When original repository assets are absent, the generator uses the
207
+ packaged snapshots. `assets/source.json` records source paths and SHA-256 hashes.
208
+ See [source asset notes](docs/source-assets.md) for the exact build boundary.
209
+
210
+ From the repository root:
211
+
212
+ ```sh
213
+ pnpm --filter @illli-studio/dsh-mory build
214
+ pnpm --filter @illli-studio/dsh-mory test
215
+ ```
216
+
217
+ Or independently from this package directory:
218
+
219
+ ```sh
220
+ npm run build
221
+ npm test
222
+ npm pack --dry-run
223
+ npm pack
224
+ ```
225
+
226
+ `prepack` rebuilds the client, runs syntax checks and runs the adapter/Host tests.
227
+ Inspect the dry-run file list for `index.js`, `service.mjs`, generated `client.js`,
228
+ `src/client.js`, `scripts/build-client.mjs`, source snapshots under `assets/`,
229
+ adapter tests, source notes, `cordis.patch.yml`, English and Chinese locale JSON,
230
+ `icon.svg`, the README and license. No secrets or local Mory data should be in
231
+ the archive.
232
+
233
+ For a release, sign in to npm with publish rights for the `@illli-studio` scope,
234
+ set the intended version in this package, complete the checks, and run:
235
+
236
+ ```sh
237
+ npm publish --access public
238
+ ```
239
+
240
+ Publishing is a separate explicit action. To appear in a plugin marketplace,
241
+ submit the published npm package to the marketplace index you intend to use;
242
+ this bundle manifest alone does not enroll it in any index.
243
+
244
+ After installation verify: sidebar → Mory, initial connection, create/search/edit
245
+ one disposable memory, concurrent edit conflict, deletion confirmation, English
246
+ and Chinese, light/dark theme, and return to conversation. Disable the bundle to
247
+ check that its sidebar entry and route are disposed. Delete your test memory
248
+ before finishing. A successful installation alone does not prove visual behavior.
249
+
250
+ ## License
251
+
252
+ AGPL-3.0-or-later, matching Mory. The stylesheet and logo are the original assets
253
+ from this Mory repository, included under the same project license. The SVG keeps
254
+ the website's font fallback behavior; no font files are embedded or downloaded.
255
+ Source: https://github.com/illli-studio/mory.
@@ -0,0 +1,88 @@
1
+ const kinds = new Set(["note", "chat", "bill", "finance", "task", "waiting", "bookmark", "file", "decision", "log"]);
2
+ const types = new Set(["raw", "preview", "snapshot", "relation"]);
3
+ const statuses = new Set(["active", "deleted", "superseded"]);
4
+ function record(value, label) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
6
+ return value;
7
+ }
8
+ function timestamp(value, fallback) {
9
+ if (value === undefined) return fallback;
10
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new Error("Invalid timestamp.");
11
+ return new Date(value).toISOString();
12
+ }
13
+ function strings(value, label) {
14
+ if (value === undefined) return [];
15
+ if (!Array.isArray(value) || value.some(v => typeof v !== "string")) throw new Error(`${label} must be a string array.`);
16
+ return [...new Set(value.map(v => v.trim()).filter(Boolean))];
17
+ }
18
+ export function stableJson(value) {
19
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
20
+ if (value && typeof value === "object") return `{${Object.entries(value).filter(([,v]) => v !== undefined).sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0).map(([k,v]) => `${JSON.stringify(k)}:${stableJson(v)}`).join(",")}}`;
21
+ return JSON.stringify(value);
22
+ }
23
+ export function memoryFingerprint(memory) {
24
+ return stableJson({source:memory.source,type:memory.type,kind:memory.kind,payload:memory.payload,
25
+ fields:memory.fields || {},metadata:memory.metadata || {},tags:[...memory.tags].sort(),
26
+ isPrivate:Boolean(memory.isPrivate),scope:memory.scope || {},status:memory.status || "active"});
27
+ }
28
+ export async function normalizeMemory(input) {
29
+ record(input,"Memory");
30
+ const payload = input.payload === undefined ? {} : record(input.payload,"Payload");
31
+ const content = payload.content ?? input.text ?? input.content ?? "";
32
+ const title = payload.title ?? input.title ?? (typeof content === "string" ? content.slice(0,72) : "");
33
+ if (typeof content !== "string" || typeof title !== "string" || (!content.trim() && !title.trim())) throw new Error("Memory needs a title or content.");
34
+ if (content.length > 1_000_000 || title.length > 4096) throw new Error("Memory exceeds size limit.");
35
+ const id = input.id ?? crypto.randomUUID();
36
+ if (typeof id !== "string" || !id.trim() || id.length > 256) throw new Error("Invalid memory id.");
37
+ const source = input.source ?? "manual";
38
+ if (typeof source !== "string" || !source.trim()) throw new Error("Invalid source.");
39
+ const kind = input.kind ?? "note", type = input.type ?? "raw", status = input.status ?? "active";
40
+ if (!kinds.has(kind) || !types.has(type) || !statuses.has(status)) throw new Error("Invalid kind, type or status.");
41
+ const metadata = record(input.metadata ?? input.fields ?? {},"Metadata");
42
+ const fields = record(input.fields ?? Object.fromEntries(Object.entries(metadata).filter(([,v]) => ["string","number","boolean"].includes(typeof v))),"Fields");
43
+ if (Object.values(fields).some(v => v !== undefined && !["string","number","boolean"].includes(typeof v))) throw new Error("Fields must contain scalar values.");
44
+ const scope = record(input.scope ?? {},"Scope");
45
+ if (Object.values(scope).some(v => typeof v !== "string")) throw new Error("Scope values must be strings.");
46
+ const now = new Date().toISOString();
47
+ const capturedAt = timestamp(input.capturedAt ?? input.createdAt,now);
48
+ const revision = input.revision ?? 0;
49
+ if (!Number.isSafeInteger(revision) || revision < 0) throw new Error("Invalid revision.");
50
+ if (input.isPrivate !== undefined && typeof input.isPrivate !== "boolean") throw new Error("Invalid privacy flag.");
51
+ const url = payload.url ?? input.url;
52
+ if (url !== undefined && (typeof url !== "string" || !/^https?:\/\//i.test(url))) throw new Error("URL must use http or https.");
53
+ const actor = input.actor ?? {type:"human"};
54
+ if (!actor || !["human","agent","ai","plugin"].includes(actor.type) || (actor.id !== undefined && typeof actor.id !== "string")) throw new Error("Invalid actor.");
55
+ const origin = input.origin;
56
+ if (origin !== undefined && (!origin || typeof origin.deviceId !== "string" || typeof origin.deviceName !== "string")) throw new Error("Invalid device origin.");
57
+ const embedding = input.embedding;
58
+ if (embedding != null && (!Array.isArray(embedding) || embedding.some(v => typeof v !== "number" || !Number.isFinite(v)))) throw new Error("Invalid embedding.");
59
+ const memory = {id,source,type,kind,status,revision,capturedAt,updatedAt:timestamp(input.updatedAt,capturedAt),
60
+ payload:{title:title.trim() || "Untitled memory",content:content.trim(),...(url ? {url} : {})},
61
+ fields,metadata,tags:strings(input.tags,"Tags").map(v => v.replace(/^#/,"").toLowerCase()),
62
+ isPrivate:input.isPrivate ?? false,score:Number.isFinite(input.score) ? input.score : 50,
63
+ preview:content.trim().replace(/\s+/g," ").slice(0,180),schemaVersion:2,actor,
64
+ ...(origin ? {origin} : {}),scope,sourceEventIds:strings(input.sourceEventIds,"Source event ids"),
65
+ ...(input.parentIds ? {parentIds:strings(input.parentIds,"Parent ids")} : {}),...(embedding ? {embedding} : {})};
66
+ const digest = await crypto.subtle.digest("SHA-256",new TextEncoder().encode(memoryFingerprint(memory)));
67
+ return {...memory,contentHash:Array.from(new Uint8Array(digest),v => v.toString(16).padStart(2,"0")).join("")};
68
+ }
69
+ export function mergeSnapshots(local, remote) {
70
+ const tombstones = new Set([...local.tombstones,...remote.tombstones]);
71
+ const byId = new Map();
72
+ for (const memory of [...remote.memories,...local.memories]) {
73
+ if (tombstones.has(memory.id)) continue;
74
+ const existing = byId.get(memory.id);
75
+ if (!existing || memoryFingerprint(existing) === memoryFingerprint(memory)) {
76
+ byId.set(memory.id,!existing || (memory.updatedAt ?? memory.capturedAt) > (existing.updatedAt ?? existing.capturedAt) ? memory : existing);
77
+ continue;
78
+ }
79
+ // Keep both divergent versions under deterministic ids, including on retries.
80
+ const a = `${existing.updatedAt ?? existing.capturedAt}:${memoryFingerprint(existing)}`;
81
+ const b = `${memory.updatedAt ?? memory.capturedAt}:${memoryFingerprint(memory)}`;
82
+ const [winner,other] = a >= b ? [existing,memory] : [memory,existing];
83
+ byId.set(memory.id,winner);
84
+ const copyId = `${memory.id.slice(0,160)}~conflict~${other.contentHash.slice(0,64)}`;
85
+ if (!tombstones.has(copyId)) byId.set(copyId,{...other,id:copyId,revision:0,parentIds:[...new Set([...(other.parentIds || []),memory.id])]});
86
+ }
87
+ return {schemaVersion:2,exportedAt:new Date().toISOString(),memories:[...byId.values()].sort((a,b) => b.capturedAt.localeCompare(a.capturedAt)),tombstones:[...tombstones].sort()};
88
+ }
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 427.87 427.87">
3
+ <defs>
4
+ <style>
5
+ .cls-1, .cls-2 {
6
+ fill: #0b3b32;
7
+ }
8
+
9
+ .cls-3 {
10
+ font-family: AdobeSongStd-Light-GBpc-EUC-H, 'Adobe Song Std';
11
+ font-weight: 300;
12
+ letter-spacing: -.1em;
13
+ }
14
+
15
+ .cls-4 {
16
+ fill: #f5f1e7;
17
+ }
18
+
19
+ .cls-5, .cls-6 {
20
+ letter-spacing: -.1em;
21
+ }
22
+
23
+ .cls-6, .cls-7 {
24
+ font-family: CooperBlack, 'Cooper Black';
25
+ }
26
+
27
+ .cls-2 {
28
+ font-size: 86px;
29
+ }
30
+
31
+ .cls-8 {
32
+ fill: #9bb5a0;
33
+ }
34
+
35
+ .cls-9 {
36
+ letter-spacing: -.15em;
37
+ }
38
+
39
+ .cls-10 {
40
+ fill: #2c6870;
41
+ }
42
+ </style>
43
+ </defs>
44
+ <rect class="cls-1" x="0" y="0" width="427.87" height="427.87" rx="102.32" ry="102.32"/>
45
+ <g>
46
+ <rect class="cls-10" x="61.29" y="68.46" width="351.95" height="290.95" rx="68.04" ry="68.04" transform="translate(-21.32 401.54) rotate(-78)"/>
47
+ <rect class="cls-8" x="25.31" y="68.46" width="358.99" height="290.95" rx="68.04" ry="68.04" transform="translate(-32.5 391.14) rotate(-83)"/>
48
+ <rect class="cls-4" x="45.86" y="45" width="286.26" height="337.88" rx="56.31" ry="56.31" transform="translate(-7.35 6.73) rotate(-2)"/>
49
+ </g>
50
+ <text class="cls-2" transform="translate(64.68 198.47) rotate(-1.97)"><tspan class="cls-6" x="0" y="0">ME</tspan><tspan class="cls-3" x="118.74" y="0">.</tspan><tspan class="cls-7"><tspan class="cls-5" x="0" y="83">MO</tspan><tspan class="cls-9" x="126.26" y="83">R</tspan><tspan class="cls-5" x="180.32" y="83">Y</tspan></tspan></text>
51
+ </svg>
@@ -0,0 +1,6 @@
1
+ {
2
+ "stylesheet": "apps/web/src/styles.css",
3
+ "logo": "apps/web/public/mory-logo-premium.svg",
4
+ "stylesheetSha256": "6f7b05e7fa7fba6b0e9f6b64f9505eb3079b65503258dd967af7d49b7a25c3b5",
5
+ "logoSha256": "066a45748d7faff7a5946870335c1ab60cee2c45d2dfe3dbaf195501e3b228ba"
6
+ }
@@ -0,0 +1,106 @@
1
+ import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
2
+
3
+ export type Language = "en" | "zh";
4
+
5
+ const translations = {
6
+ en: {
7
+ settings: "Settings", localRepository: "Local memory repository", storehouse: "Storehouse", recordTypes: "Record types",
8
+ allMemories: "All memories", newMemory: "New memory", search: "Search memories...", today: "today", topics: "topics", pageSize: "Per page", previousPage: "Previous page", nextPage: "Next page", pageOf: "of",
9
+ importMemories: "Import memories", repositorySummary: "Repository summary", repositoryActions: "Repository actions",
10
+ captureTitle: "Capture something worth keeping", loading: "Loading your local repository...", reading: "Reading memories from IndexedDB.",
11
+ clearSearch: "Clear search", time: "Time", entity: "Entity", memoryContent: "Memory content", category: "Category", lifecycle: "Lifecycle",
12
+ repositorySettings: "Repository settings", manageSettings: "Manage connections and repository tools in one place.", settingsSections: "Settings sections",
13
+ language: "Language", languageSwitch: "Language switch", english: "English", chinese: "中文", languageHint: "Choose the language for the interface.",
14
+ allSources: "All sources", manual: "Manual", clipboard: "Clipboard", browser: "Browser", file: "File", chat: "Chat", github: "GitHub",
15
+ note: "Note", finance: "Finance", bill: "Bill", task: "Task", waiting: "Waiting", bookmark: "Bookmark", decision: "Decision", log: "Log",
16
+ save: "Save", saved: "Saved", capture: "Capture", close: "Close", retry: "Retry",
17
+ noMatch: "No memories match this filter.", waitingRepo: "Your memory repository is waiting.",
18
+ nothingMatched: "Nothing matched", tryAnother: "Try another search or clear the filter.", firstObject: "Capture the first object above and this area becomes your living memory feed.",
19
+ anyTime: "Any time", last7: "Last 7 days", last30: "Last 30 days", allVisibility: "All visibility", publicOnly: "Public only", privateOnly: "Private only", newest: "Newest first", score: "Score first",
20
+ health: "Health", topicsLabel: "Topics", connectors: "Connectors", languageSetting: "Interface language", theme: "Appearance", themeHint: "Choose a visual style for your workspace.", themeDefault: "Default", themeDefaultHint: "Calm repository green", themeMidnight: "Midnight", themeMidnightHint: "Focused dark workspace", themeOcean: "Ocean", themeOceanHint: "Cool blue clarity", themeSunset: "Sunset", themeSunsetHint: "Warm amber energy", themeForest: "Forest", themeForestHint: "Deep natural focus", themeLavender: "Lavender", themeLavenderHint: "Soft creative calm", themeGraphite: "Graphite", themeGraphiteHint: "Neutral dark precision", themeRose: "Rose", themeRoseHint: "Warm editorial blush", themeTerminal: "Terminal", themeTerminalHint: "High-contrast green glow", themeSand: "Sand", themeSandHint: "Quiet paper workspace", themeCobalt: "Cobalt", themeCobaltHint: "Electric blue focus", themePlum: "Plum", themePlumHint: "Rich evening editorial", themeMint: "Mint", themeMintHint: "Fresh lightweight clarity", themeCopper: "Copper", themeCopperHint: "Crafted warm contrast", themeMono: "Mono", themeMonoHint: "Quiet black and white", themeAbyss: "Abyss", themeAbyssHint: "Deep blue-black focus", themeEmber: "Ember", themeEmberHint: "Warm firelit workspace", themeAurora: "Aurora", themeAuroraHint: "Teal light in the dark", themeViolet: "Violet", themeVioletHint: "Deep creative night", themeNavy: "Navy", themeNavyHint: "Clear midnight blue", themeCrimson: "Crimson", themeCrimsonHint: "Bold dark red focus", themeSapphire: "Sapphire", themeSapphireHint: "Deep blue clarity", themeMoss: "Moss", themeMossHint: "Grounded green shadow", themeSolar: "Solar", themeSolarHint: "Golden light after dark", themeInk: "Ink", themeInkHint: "Minimal editorial black",
21
+ selectAll: "Select all visible memories", memoryDetail: "Memory details", structuredFields: "Structured Fields", preview: "Preview", selectionPack: "Selection Pack",
22
+ addToPack: "Add to Pack", removeFromPack: "Remove from Pack", makePublic: "Make Public", markPrivate: "Mark Private", copyPack: "Copy pack", copied: "Copied",
23
+ repositoryHealth: "Repository Health", repositoryActive: "Repository active", projectsTopics: "Projects & Topics", topicHint: "Capture or import more memories and Mory will cluster them by tag.",
24
+ githubSync: "Memory Sync", setupFlow: "Setup guide", setupGuide: "Setup guide", mergePush: "Merge & Push", push: "Push", pull: "Pull", backup: "Commit SQLite backup", connectorsTitle: "Connectors", provider: "Provider", gitee: "Gitee", autoSync: "Auto backup", deviceName: "Device name", autoSyncHint: "Automatically merge and back up after changes.", syncConnection: "Repository connection", syncConnectionHint: "Choose where your memories will be stored.", repositoryUrl: "Repository URL", checkConnection: "Check connection", checkingConnection: "Checking...", connectionSuccess: "Connection successful", syncLocal: "Local", syncSyncing: "Syncing", syncSynced: "Synced", syncError: "Sync needs attention", untitledMemory: "Untitled memory", syncAccess: "Access & device", syncAccessHint: "Use the same repository on multiple devices.",
25
+ import: "Import", export: "Export", paste: "Paste", title: "Title", content: "Content", tags: "Tags", source: "Source", type: "Type", saveChanges: "Save changes",
26
+ delete: "Delete", active: "Active", noDueDate: "no due date", unknownMerchant: "Unknown merchant", conversationRecord: "Conversation record", webReference: "Web reference", fileReference: "File reference",
27
+ clear: "Clear", clearSelection: "Clear selection", compact: "Compact", full: "Full", archive: "Archive", included: "included", privateExcluded: "private excluded", portableBundle: "Copy selected memories as a portable text bundle.",
28
+ githubTokenNote: "The token is stored on this device so automatic sync can continue after refresh.", pushRemote: "Push remote repository", pullRemote: "Pull remote repository",
29
+ clipboardLoaded: "Clipboard loaded.", clipboardPermission: "Allow clipboard permission, then try again.",
30
+ tagsPlaceholder: "tags: product, mory, research", amountPlaceholder: "Amount, e.g. 68.90", merchantPlaceholder: "Merchant or payee", categoryPlaceholder: "Category, e.g. food, infra, travel", conversationPlaceholder: "Conversation with / channel / room", currencyPlaceholder: "Currency", accountPlaceholder: "Account / asset / wallet", typePlaceholder: "Type: income, asset, debt, transfer", ownerProject: "Owner / project", waitingPlaceholder: "Waiting for person / event / reply", fileNamePlaceholder: "File name", pathPlaceholder: "Path, drive URL, or storage location", ownerTeamProject: "Owner / team / project", moodPlaceholder: "Mood, state, metric, or location",
31
+ todo: "Todo", doing: "Doing", done: "Done", blocked: "Blocked", followUp: "Follow-up", received: "Received", proposed: "Proposed", decided: "Decided", reversed: "Reversed",
32
+ tagsEmerge: "Tags will emerge as you capture.", importBookmarks: "Import bookmarks", importText: "Import text", importJson: "Import JSON", exportMemories: "Export memories", rawMemory: "Raw memory", openMemory: "Open memory", selectMemory: "Select memory",
33
+ conversationTitlePlaceholder: "Conversation title, or leave it for Mory to infer", billTitlePlaceholder: "Bill title, project, or subscription name", bookmarkTitlePlaceholder: "Page title, article name, or reference label", financeTitlePlaceholder: "Financial record title, account, asset, or transaction", taskTitlePlaceholder: "Task title or action item", waitingTitlePlaceholder: "Waiting item or follow-up title", fileTitlePlaceholder: "Document title or file reference", decisionTitlePlaceholder: "Decision title", logTitlePlaceholder: "Log title, habit, day, project, or event", noteTitlePlaceholder: "Give this memory a name, or leave it for Mory to infer",
34
+ chatContentPlaceholder: "Write messages, decisions, or conversation fragments. Example:\nMe: ...\nPartner: ...", billContentPlaceholder: "Optional bill note: why it happened, project relation, receipt detail, reimbursement status...", bookmarkContentPlaceholder: "Optional page note: why this link matters, what to remember, related project...", financeContentPlaceholder: "Optional finance note: why it changed, source, account detail, tax/reimbursement context...", taskContentPlaceholder: "Describe the task, acceptance criteria, blockers, or next step...", waitingContentPlaceholder: "Record what you are waiting for, what happened, and when to follow up...", fileContentPlaceholder: "Describe what this file contains, where it belongs, and why it matters...", decisionContentPlaceholder: "Record the decision, options considered, reason, owner, and consequences...", logContentPlaceholder: "Write what happened, metrics, state, observation, or timeline notes...", noteContentPlaceholder: "Paste a decision, page note, meeting context, link, or fragment worth remembering.",
35
+ justNow: "just now", minutesAgo: "m ago", hoursAgo: "h ago", daysAgo: "d ago", clearPack: "Remove all memories from the current selection pack", deleteSelected: "Delete all selected memories", copyBundle: "Copy selected memories as a structured text bundle",
36
+ objects: "Objects", objectCount: "objects", tagsCount: "Tags", planned: "Planned", ready: "Ready", activeLite: "Active-lite", privateSyncWarning: "private memories are included in sync/export unless you remove them first.", sqliteBackingUp: "Backing up SQLite...", sqliteCommitted: "SQLite + JSON committed", sqliteBackupFailed: "SQLite backup failed.",
37
+ loadFailed: "Failed to load memories.", saveFailed: "Failed to save memory.", importFailed: "Import failed.", githubConfig: "GitHub sync needs token, owner, repo, branch and path.", pushing: "Pushing memories to GitHub...", pulling: "Pulling memories from the remote repository...", merging: "Merging local and remote memories...", pushedObjects: "Pushed {count} memory objects to {provider}.", pulledObjects: "Pulled {count} memory objects from {provider}.", mergedObjects: "Merged and pushed {count} memory objects.", githubPushFailed: "GitHub push failed.", githubPullFailed: "GitHub pull failed.", githubMergeFailed: "GitHub merge failed.",
38
+ },
39
+ zh: {
40
+ settings: "设置", localRepository: "本地记忆仓库", storehouse: "记忆库", recordTypes: "记录类型",
41
+ allMemories: "全部记忆", newMemory: "新建记忆", search: "搜索记忆...", today: "今天", topics: "主题", pageSize: "每页", previousPage: "上一页", nextPage: "下一页", pageOf: "/",
42
+ importMemories: "导入记忆", repositorySummary: "仓库摘要", repositoryActions: "仓库操作",
43
+ captureTitle: "记录值得保留的内容", loading: "正在加载本地仓库...", reading: "正在从 IndexedDB 读取记忆。",
44
+ clearSearch: "清除搜索", time: "时间", entity: "实体", memoryContent: "记忆内容", category: "分类", lifecycle: "生命周期",
45
+ repositorySettings: "仓库设置", manageSettings: "在这里管理连接和仓库工具。", settingsSections: "设置分区",
46
+ language: "语言", languageSwitch: "语言切换", english: "English", chinese: "中文", languageHint: "选择界面显示语言。",
47
+ allSources: "全部来源", manual: "手动", clipboard: "剪贴板", browser: "浏览器", file: "文件", chat: "对话", github: "GitHub",
48
+ note: "笔记", finance: "财务", bill: "账单", task: "任务", waiting: "等待中", bookmark: "书签", decision: "决策", log: "日志",
49
+ save: "保存", saved: "已保存", capture: "记录", close: "关闭", retry: "重试",
50
+ noMatch: "没有记忆符合当前筛选条件。", waitingRepo: "你的记忆仓库正在等待内容。",
51
+ nothingMatched: "没有找到", tryAnother: "试试其他搜索词,或清除筛选条件。", firstObject: "记录上方的第一条内容,这里就会成为你的记忆流。",
52
+ anyTime: "不限时间", last7: "最近 7 天", last30: "最近 30 天", allVisibility: "全部可见性", publicOnly: "仅公开", privateOnly: "仅私密", newest: "最新优先", score: "评分优先",
53
+ health: "健康状态", topicsLabel: "主题", connectors: "连接器", languageSetting: "界面语言", theme: "外观主题", themeHint: "选择适合你的工作区视觉风格。", themeDefault: "默认主题", themeDefaultHint: "沉静的仓库绿", themeMidnight: "午夜", themeMidnightHint: "专注的深色工作区", themeOcean: "海洋", themeOceanHint: "清爽的蓝色界面", themeSunset: "日落", themeSunsetHint: "温暖的琥珀色能量", themeForest: "森林", themeForestHint: "深沉的自然专注感", themeLavender: "薰衣草", themeLavenderHint: "柔和的创意氛围", themeGraphite: "石墨", themeGraphiteHint: "中性的深色精确感", themeRose: "玫瑰", themeRoseHint: "温暖的编辑感腮红", themeTerminal: "终端", themeTerminalHint: "高对比的绿色荧光", themeSand: "砂纸", themeSandHint: "安静的纸张工作区", themeCobalt: "钴蓝", themeCobaltHint: "电光蓝的专注感", themePlum: "梅紫", themePlumHint: "浓郁的夜间编辑感", themeMint: "薄荷", themeMintHint: "清新的轻盈明亮感", themeCopper: "铜色", themeCopperHint: "手作感的温暖对比", themeMono: "黑白", themeMonoHint: "安静的黑白工作区", themeAbyss: "深渊", themeAbyssHint: "深蓝黑的专注感", themeEmber: "余烬", themeEmberHint: "温暖的火光工作区", themeAurora: "极光", themeAuroraHint: "暗夜中的青绿色光", themeViolet: "紫夜", themeVioletHint: "深色的创意夜景", themeNavy: "海军蓝", themeNavyHint: "清晰的午夜蓝", themeCrimson: "绯红", themeCrimsonHint: "醒目的深红专注感", themeSapphire: "蓝宝石", themeSapphireHint: "深邃的蓝色清晰感", themeMoss: "苔藓", themeMossHint: "沉稳的绿色阴影", themeSolar: "日冕", themeSolarHint: "暗夜中的金色光线", themeInk: "墨色", themeInkHint: "极简的编辑黑",
54
+ selectAll: "选择当前可见的全部记忆", memoryDetail: "记忆详情", structuredFields: "结构化字段", preview: "预览", selectionPack: "选择包",
55
+ addToPack: "加入选择包", removeFromPack: "移出选择包", makePublic: "设为公开", markPrivate: "标记为私密", copyPack: "复制选择包", copied: "已复制",
56
+ repositoryHealth: "仓库健康状态", repositoryActive: "仓库运行正常", projectsTopics: "项目与主题", topicHint: "继续记录或导入更多记忆,Mory 会按标签聚合它们。",
57
+ githubSync: "记忆同步", setupFlow: "配置流程", setupGuide: "配置教程", mergePush: "合并并推送", push: "推送", pull: "拉取", backup: "提交 SQLite 备份", connectorsTitle: "连接器", provider: "服务商", gitee: "Gitee", autoSync: "自动备份", deviceName: "设备名称", autoSyncHint: "记忆发生变化后自动合并并备份。", syncConnection: "连接仓库", syncConnectionHint: "选择用于保存记忆的远程仓库。", repositoryUrl: "仓库地址", checkConnection: "检测连接", checkingConnection: "检测中…", connectionSuccess: "连接成功", syncLocal: "仅本地", syncSyncing: "同步中", syncSynced: "已同步", syncError: "同步需处理", untitledMemory: "未命名记忆", syncAccess: "访问与设备", syncAccessHint: "同一个仓库可以在多台设备上使用。",
58
+ import: "导入", export: "导出", paste: "粘贴", title: "标题", content: "内容", tags: "标签", source: "来源", type: "类型", saveChanges: "保存修改",
59
+ delete: "删除", active: "活跃", noDueDate: "无截止日期", unknownMerchant: "未知商户", conversationRecord: "对话记录", webReference: "网页引用", fileReference: "文件引用",
60
+ clear: "清除", clearSelection: "清除选择", compact: "精简", full: "完整", archive: "归档", included: "条已包含", privateExcluded: "条私密记忆已排除", portableBundle: "将选中的记忆复制为可携带的文本包。",
61
+ githubTokenNote: "Token 会保存在本设备,用于刷新后继续自动同步。", pushRemote: "推送到远程仓库", pullRemote: "从远程仓库拉取",
62
+ clipboardLoaded: "剪贴板内容已加载。", clipboardPermission: "请允许剪贴板权限后重试。",
63
+ tagsPlaceholder: "标签:产品、Mory、研究", amountPlaceholder: "金额,例如 68.90", merchantPlaceholder: "商户或收款方", categoryPlaceholder: "分类,例如餐饮、基础设施、出行", conversationPlaceholder: "对话对象 / 频道 / 房间", currencyPlaceholder: "货币", accountPlaceholder: "账户 / 资产 / 钱包", typePlaceholder: "类型:收入、资产、负债、转账", ownerProject: "负责人 / 项目", waitingPlaceholder: "等待的人 / 事件 / 回复", fileNamePlaceholder: "文件名", pathPlaceholder: "路径、云盘 URL 或存储位置", ownerTeamProject: "负责人 / 团队 / 项目", moodPlaceholder: "情绪、状态、指标或地点",
64
+ todo: "待办", doing: "进行中", done: "已完成", blocked: "已阻塞", followUp: "待跟进", received: "已收到", proposed: "提议中", decided: "已决定", reversed: "已撤回",
65
+ tagsEmerge: "记录更多内容后,标签会逐渐出现。", importBookmarks: "导入书签", importText: "导入文本", importJson: "导入 JSON", exportMemories: "导出记忆", rawMemory: "原始记忆", openMemory: "打开记忆", selectMemory: "选择记忆",
66
+ conversationTitlePlaceholder: "对话标题,也可以留空让 Mory 自动推断", billTitlePlaceholder: "账单标题、项目或订阅名称", bookmarkTitlePlaceholder: "页面标题、文章名称或引用标签", financeTitlePlaceholder: "财务记录标题、账户、资产或交易", taskTitlePlaceholder: "任务标题或行动项", waitingTitlePlaceholder: "等待事项或跟进标题", fileTitlePlaceholder: "文档标题或文件引用", decisionTitlePlaceholder: "决策标题", logTitlePlaceholder: "日志标题、习惯、日期、项目或事件", noteTitlePlaceholder: "给这条记忆起个名字,也可以留空让 Mory 自动推断",
67
+ chatContentPlaceholder: "记录消息、决定或对话片段。例如:\n我:……\n对方:……", billContentPlaceholder: "可选的账单备注:发生原因、项目关联、收据详情、报销状态……", bookmarkContentPlaceholder: "可选的页面备注:链接为何重要、需要记住什么、关联项目……", financeContentPlaceholder: "可选的财务备注:变动原因、来源、账户详情、税务或报销背景……", taskContentPlaceholder: "描述任务、验收标准、阻塞原因或下一步……", waitingContentPlaceholder: "记录正在等待什么、发生了什么,以及何时跟进……", fileContentPlaceholder: "描述文件内容、归属位置以及它为何重要……", decisionContentPlaceholder: "记录决定、考虑过的选项、原因、负责人和后果……", logContentPlaceholder: "记录发生了什么、指标、状态、观察或时间线……", noteContentPlaceholder: "粘贴值得记住的决定、页面备注、会议上下文、链接或片段。",
68
+ justNow: "刚刚", minutesAgo: "分钟前", hoursAgo: "小时前", daysAgo: "天前", clearPack: "移除当前选择包中的全部记忆", deleteSelected: "删除所有选中的记忆", copyBundle: "复制选中的记忆文本包",
69
+ objects: "对象", objectCount: "个对象", tagsCount: "标签", planned: "计划中", ready: "已就绪", activeLite: "轻量活跃", privateSyncWarning: "条私密记忆会被包含在同步或导出中,如不需要请先移除。", sqliteBackingUp: "正在备份 SQLite……", sqliteCommitted: "SQLite + JSON 已提交", sqliteBackupFailed: "SQLite 备份失败。",
70
+ loadFailed: "加载记忆失败。", saveFailed: "保存记忆失败。", importFailed: "导入失败。", githubConfig: "GitHub 同步需要填写 token、所有者、仓库、分支和路径。", pushing: "正在推送记忆到 GitHub……", pulling: "正在从远程仓库拉取记忆……", merging: "正在合并本地与远程记忆……", pushedObjects: "已推送 {count} 条记忆到 {provider}。", pulledObjects: "已从 {provider} 拉取 {count} 条记忆。", mergedObjects: "已合并并推送 {count} 条记忆。", githubPushFailed: "GitHub 推送失败。", githubPullFailed: "GitHub 拉取失败。", githubMergeFailed: "GitHub 合并失败。",
71
+ },
72
+ } as const;
73
+
74
+ export type TranslationKey = keyof typeof translations.en;
75
+ type LanguageContextValue = { language: Language; setLanguage: (language: Language) => void; t: (key: TranslationKey) => string };
76
+ const LanguageContext = createContext<LanguageContextValue | null>(null);
77
+
78
+ export function LanguageProvider({ children }: { children: ReactNode }) {
79
+ const [language, setLanguageState] = useState<Language>(loadLanguage);
80
+ const setLanguage = (next: Language) => {
81
+ try { localStorage.setItem("mory.language", next); } catch { /* preference storage is optional */ }
82
+ setLanguageState(next);
83
+ };
84
+ const value = useMemo(() => ({ language, setLanguage, t: (key: TranslationKey) => translations[language][key] }), [language]);
85
+ return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
86
+ }
87
+
88
+ function loadLanguage(): Language {
89
+ try {
90
+ const saved = localStorage.getItem("mory.language");
91
+ if (saved === "zh" || saved === "en") return saved;
92
+
93
+ // Respect the browser's language only for the first visit. Once the user
94
+ // changes the setting, the explicit preference above remains authoritative.
95
+ const browserLanguages = navigator.languages?.length ? navigator.languages : [navigator.language];
96
+ return browserLanguages.some((language) => /^zh(?:-|$)/i.test(language)) ? "zh" : "en";
97
+ } catch {
98
+ return "en";
99
+ }
100
+ }
101
+
102
+ export function useLanguage() {
103
+ const value = useContext(LanguageContext);
104
+ if (!value) throw new Error("useLanguage must be used inside LanguageProvider");
105
+ return value;
106
+ }