@hiveku-apps/sync 0.3.6 → 0.5.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.
package/src/init.mjs CHANGED
@@ -3,14 +3,19 @@
3
3
  * .mcp.json, CLAUDE.md, and the empty memory/rules/skills/ tree. Also appends
4
4
  * the folder to the sibling hiveku.code-workspace file if one exists at the
5
5
  * parent directory.
6
+ *
7
+ * CLAUDE.md is the user's once written, except its folder layout and memory
8
+ * rules: they sit between markers that `pull` and `codex` refresh
9
+ * (claude-md.mjs), so a folder an older release made learns the new ones.
6
10
  */
7
11
 
8
12
  import { existsSync } from 'node:fs';
9
13
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
- import { basename, dirname, join, resolve } from 'node:path';
14
+ import { basename, join, resolve } from 'node:path';
11
15
 
12
16
  import { writeFileAtomic, safeSlug, TYPE_TO_FOLDER, loadDotenv } from './util.mjs';
13
17
  import { writeCodexScaffold, preTrustFolder } from './codex.mjs';
18
+ import { claudeMdMemoryRegion, refreshClaudeMd } from './claude-md.mjs';
14
19
 
15
20
  const VALID_PROFILES = ['full', 'sales', 'marketing', 'helpdesk', 'pm', 'dev'];
16
21
  const DEFAULT_BASE_URL = 'https://core.hiveku.com';
@@ -58,45 +63,9 @@ This folder is bound to a single Hiveku account via \`.env\` (\`OLYMPUS_API_KEY\
58
63
  The Hiveku MCP server at \`core.hiveku.com/mcp\` is wired up in \`.mcp.json\` and
59
64
  exposes the **${profile}** profile of Olympus tools.
60
65
 
61
- ## Folder layout
66
+ ${claudeMdMemoryRegion()}
62
67
 
63
- - \`memory/\` — synced memory documents (\`hiveku-sync pull\` writes here)
64
- - \`rules/\` — synced agent rules
65
- - \`skills/\` — synced agent skills
66
- - \`commands/\`, \`agents/\`, \`identity/\` — other memory types
67
- - \`hiveku-data/account/ACCOUNT_MEMORY.md\` — the account memory (the business
68
- facts every department agent reads), **read-only**: owners and admins edit it on
69
- the Hiveku dashboard under Account memory, and nothing uploads this file
70
- - \`projects/<slug>/\` — coder project source pulled via \`hiveku-sync pull-project\`
71
- - \`.hiveku/manifest.json\` — memory last-sync record
72
- - \`.hiveku/projects.json\` — project ID ↔ folder mapping
73
-
74
- When asked about this account, read the local files first — they are the
75
- authoritative copy of what was in Olympus the last time \`hiveku-sync pull\`
76
- ran. To pick up edits made elsewhere, run \`hiveku-sync pull\`.
77
-
78
- ## Memory writes
79
-
80
- The local memory files are read-only as far as Olympus is concerned. To persist
81
- changes (new skill, updated rule, etc.), call the corresponding MCP tool:
82
-
83
- - \`memory_create\` / \`memory_update\` — for memory documents, skills, rules,
84
- commands, agents, identity entries
85
- - \`memory_delete\` — to remove an entry
86
-
87
- \`memory_update\` replaces the whole entry. Each department's memory is ONE
88
- document, so read the current one with \`memory_list({ domain })\`, merge your
89
- change into its full text, and send the whole document. Use \`memory_create\`
90
- only when none exists; a 409 means one does, so read and merge, never overwrite.
91
-
92
- The account memory is the exception: no tool sets it. Read it with
93
- \`account_memory_get\`, suggest one line with \`account_memory_append\` (an owner
94
- keeps or removes it), and tell the user that owners and admins edit it on the
95
- Hiveku dashboard under Account memory.
96
-
97
- Then run \`hiveku-sync pull\` to refresh the local copies.
98
-
99
- ## Coder projects (pull / status / push)
68
+ ## Coder projects (pull / push / versions)
100
69
 
101
70
  Each account has zero or more coder projects (Next.js, Vite, Python Lambda,
102
71
  Expo). To work on one locally:
@@ -105,25 +74,54 @@ Expo). To work on one locally:
105
74
  hiveku-sync projects # list the account's projects
106
75
  hiveku-sync pull-project <slug-or-id> # snapshot into ./projects/<slug>/
107
76
  cd projects/<slug>
108
- # follow the run instructions in RUN.md (npm install && npm run dev for nextjs, etc.)
77
+ # follow the run instructions in .hiveku/RUN.md (npm install && npm run dev for nextjs, etc.)
109
78
  \`\`\`
110
79
 
111
- This is **not a git clone** — the local folder is a snapshot. To check whether
112
- edits are in sync with the Hiveku DB, and to commit changes back to the
113
- project's connected GitHub branch (same path the v3 sidebar uses):
80
+ This is **not a git clone** — the local folder is a snapshot of **Your site**
81
+ (the project's main source; \`--branch <name>\` pulls a branch instead).
82
+ \`push\` saves your edits to Hiveku and then saves them as ONE **version**, which
83
+ can be rolled back later:
114
84
 
115
85
  \`\`\`bash
116
- hiveku-sync status # diff local vs Hiveku DB
117
- hiveku-sync push -m "fix: header overlap" # save + commit changed files via MCP
86
+ hiveku-sync status # diff local vs Hiveku
87
+ hiveku-sync push -m "Updated the pricing section on the Home page"
88
+ hiveku-sync versions # the version history, newest first
89
+ hiveku-sync rollback <version id> # dry run: what going back would change
90
+ hiveku-sync deploy production # publishing is always its own step
118
91
  \`\`\`
119
92
 
120
- You can also ask Claude in this workspace to:
121
- - "Pull project <slug>"
122
- - "Show me which files are out of sync with Hiveku"
123
- - "Commit the changes in projects/<slug> to the dev branch with message '...'"
124
-
125
- Claude has the \`project_files_list\`, \`project_file_get\`, \`project_files_status\`,
126
- and \`project_commit\` MCP tools available under the **${profile}** profile.
93
+ - **Version names are read by the site owner.** Describe what changed for their
94
+ visitors ("Added a contact form to the About page"): never file paths, file
95
+ extensions, \`fix:\`-style prefixes or tool names. A version holds everything
96
+ waiting on Your site, including the Hiveku AI's and the editor's changes, so
97
+ without \`-m\` push names it from all of that; \`--no-version\` skips it.
98
+ - **One version per change the owner would recognise**: push as often as you
99
+ like with \`--no-version\`, then save the version once (\`hiveku-sync commit -m "..."\`)
100
+ before you deploy.
101
+ - **Exit codes:** 0 done; 1 some files did not save, or the push was refused
102
+ (listed; no version, no deploy); 2 every file IS saved but the version was not
103
+ (it prints the \`hiveku-sync commit\` that retries it); 3 the files and the
104
+ version ARE saved and only the deploy failed.
105
+ - **On a branch, someone else may have saved since you pulled.** push then
106
+ refuses and lists the files that may hold their work: pull again (it replaces
107
+ your local copies, so keep your edits somewhere first), or \`--force\` to
108
+ overwrite. A pull moves the files an earlier pull wrote that are not on what
109
+ it pulls now (another branch's, or deleted on Hiveku) out of the project folder
110
+ into \`.hiveku/set-aside/\`, so they are never pushed back by mistake.
111
+ - **Rolling back** writes a NEW version equal to the old one, so it can be undone,
112
+ and it never publishes. Run the dry run, show the owner what it changes, and only
113
+ after they agree run the \`--apply\` command it prints. Then offer \`deploy\`
114
+ separately.
115
+
116
+ Through the MCP tools the same loop is: save (\`project_files_bulk_save\` /
117
+ \`project_file_save\`), verify, then \`project_vcs_commit({ project_id, message })\`
118
+ with **no files** (one version, plain-language name; 409 \`nothing_to_commit\`
119
+ means it is already a version), then \`deploy_site\`.
120
+ \`project_vcs_status\` says whether changes are waiting to be saved as a version;
121
+ \`project_vcs_rollback\` rolls back (dry run by default; applying needs the dry
122
+ run's \`head_commit_id\` as \`expected_head_commit_id\`, and on Your site its
123
+ \`live_fingerprint\` as \`expected_live_fingerprint\`). The \`project_*\` tools come
124
+ with the \`dev\` and \`full\` profiles (this folder uses **${profile}**).
127
125
 
128
126
  ## When Hiveku itself gets in your way
129
127
 
@@ -233,6 +231,9 @@ export async function runInit({ slug, parentDir, profile = 'full', codex = false
233
231
  * .codex/config.toml + knowledge→skills). Run from inside the folder, or pass
234
232
  * one. Derives the account slug from the folder name and the profile from
235
233
  * .env (HIVEKU_PROFILE), and inlines the real OLYMPUS_API_KEY when .env has it.
234
+ * AGENTS.md sends Codex to CLAUDE.md first, so the part of CLAUDE.md that
235
+ * hiveku-sync keeps current is refreshed too (claude-md.mjs), after the
236
+ * scaffold is written: a refused config.toml update changes nothing.
236
237
  */
237
238
  export async function runCodex({ folder, trust = false } = {}) {
238
239
  const target = folder ? resolve(folder) : process.cwd();
@@ -243,5 +244,11 @@ export async function runCodex({ folder, trust = false } = {}) {
243
244
  const slug = safeSlug(basename(target));
244
245
  const result = await writeCodexScaffold({ folder: target, slug, baseUrl, profile });
245
246
  if (trust) result.trust = await preTrustFolder(target);
246
- return { folder: target, slug, profile, ...result };
247
+ let claudeMd;
248
+ try {
249
+ claudeMd = await refreshClaudeMd(target);
250
+ } catch (err) {
251
+ claudeMd = { status: 'failed', message: err.message };
252
+ }
253
+ return { folder: target, slug, profile, ...result, claudeMd };
247
254
  }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Where `pull` writes each memory entry: filed by its OWNER, the way the
3
+ * Memory page files it (memory surfaces audit G14).
4
+ *
5
+ * <type folder>/<owner>/<name>.md an agent's own entry: rules/sales/,
6
+ * skills/seo/, memory/helpdesk/, ...
7
+ * <type folder>/shared/<name>.md an entry no agent owns: the page's
8
+ * "Shared with every agent"
9
+ *
10
+ * The owner is the `owner` field memory_list returns (the builder's one owner
11
+ * rule); an older builder that does not return it gets the same rule from
12
+ * memory-owner.mjs. A Marketing topic is its own owner (rules/seo/,
13
+ * rules/ppc/), and `marketing` is the Marketing parent.
14
+ *
15
+ * <type folder>/<owner>/not-followed/<name>.md
16
+ * an entry filed under an owner that no
17
+ * agent reads, which the Memory page
18
+ * hides: a rule, skill or note filed for
19
+ * the chief of staff outside her
20
+ * `_account:*` rows, an owner that is no
21
+ * agent (`mini`), a note a department
22
+ * owns under a name its agent never
23
+ * reads. memory_list's `placement` says
24
+ * so (hidden, a reason other than
25
+ * `system`), or ownedPlacement does.
26
+ * Kept out of the Codex rules digest and
27
+ * skill mirror, which read one folder
28
+ * level only.
29
+ *
30
+ * The `_account:*` rows are not an agent's own entries. They are placed the
31
+ * way the page shows them (memory_list's `placement`, or accountPlacement),
32
+ * and written read-only, because owners and admins change them on the Memory
33
+ * page:
34
+ *
35
+ * memory/orchestrator/how-it-works.md `_account:soul` (the chief of staff's How it works)
36
+ * memory/orchestrator/background.md `_account:claude` (her Background)
37
+ * rules/orchestrator/<slug>.md `_account:_rule:<slug>`
38
+ * skills/orchestrator/<slug>.md `_account:_skill:<slug>`
39
+ * memory/orchestrator/<topic>.md `_account:memory:<topic>` (her notes)
40
+ * memory/orchestrator/moved/<topic>.md the five topics now kept in About your
41
+ * business: her old copies, each opening
42
+ * with a line that says where the current
43
+ * text is
44
+ * hiveku-data/account/voice/<name>.md Voice and pronunciation (About your business)
45
+ *
46
+ * Rows the page never shows (internal state, an old `_account:*` shape) are
47
+ * not written. Pure: it plans the files; pull.mjs writes them.
48
+ */
49
+
50
+ import { createHash } from 'node:crypto';
51
+
52
+ import { accountMemoryDashboardUrl, ACCOUNT_DATA_DIR, ACCOUNT_MEMORY_FILE } from './account-memory.mjs';
53
+ import {
54
+ SHARED_OWNER,
55
+ accountPlacement,
56
+ accountPlacementLabel,
57
+ isAccountMemoryRow,
58
+ isAccountScopedRow,
59
+ isSystemRow,
60
+ memoryTypeOf,
61
+ movedTopicHeading,
62
+ ownedPlacement,
63
+ ownerOf,
64
+ } from './memory-owner.mjs';
65
+ import { safeSlug, toFrontmatter, TYPE_TO_FOLDER } from './util.mjs';
66
+
67
+ /** Voice and pronunciation, under About your business (read-only, never uploaded: push leaves hiveku-data/account/ out). */
68
+ export const VOICE_DIR = `${ACCOUNT_DATA_DIR}/voice`;
69
+
70
+ /** The subfolder of an owner's folder that holds the entries no agent reads (the page hides them). */
71
+ export const NOT_FOLLOWED_DIR = 'not-followed';
72
+
73
+ /** The page's words for a row it does not show (builder memory-placement.ts NOT_ON_MEMORY_PAGE). */
74
+ const NOT_SHOWN_LABEL = 'Not shown on the Memory page';
75
+
76
+ /**
77
+ * The line a moved topic's file opens with: where the topic went, where the
78
+ * current text is, and that this is the chief of staff's old copy. True
79
+ * whether or not the topic was copied across yet (the page says which).
80
+ */
81
+ export function movedTopicPointer(domain) {
82
+ const heading = movedTopicHeading(domain);
83
+ const where = heading ? `Moved to About your business, under "${heading}".` : 'Moved to About your business.';
84
+ return `> ${where} The current text is in \`${ACCOUNT_MEMORY_FILE}\`. This file is the chief of staff's old copy and may be out of date.`;
85
+ }
86
+
87
+ /** Owners whose name cannot be a folder file under this one (the true owner stays in the front matter). */
88
+ export const OTHER_OWNER_FOLDER = 'other';
89
+
90
+ /** A lowercase name that is a folder on every system: a letter first, 50 characters at most. */
91
+ const FOLDER_NAME = /^[a-z][a-z0-9_-]{0,49}$/;
92
+
93
+ /** Names Windows keeps for devices, with or without an extension: no folder or file may use one. */
94
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$/i;
95
+
96
+ /**
97
+ * File names an agent tool would read as its own instructions: Claude Code
98
+ * loads a folder's CLAUDE.md when it reads files there, and Codex reads
99
+ * AGENTS.md. On a case-insensitive disk `claude.md` is that file, so the chief
100
+ * of staff's Background (`_account:claude`) or a note named `claude` must
101
+ * never be written under that name.
102
+ */
103
+ const INSTRUCTION_FILE_NAME = /^(?:claude|agents)(?:\..*)?$/i;
104
+
105
+ /** Added after the first part of a file name that is kept (the Hiveku plugin's DEVICE_STEM_SUFFIX). */
106
+ const KEPT_NAME_SUFFIX = '-entry';
107
+
108
+ /** The folder an owner's entries go in: `shared` for no owner. */
109
+ export function ownerFolder(owner) {
110
+ if (owner === null || owner === undefined || owner === '') return SHARED_OWNER;
111
+ const clean = String(owner)
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9_-]+/g, '_')
114
+ .replace(/^[^a-z]+/, '')
115
+ .slice(0, 50);
116
+ return FOLDER_NAME.test(clean) && !WINDOWS_DEVICE_NAME.test(clean) ? clean : OTHER_OWNER_FOLDER;
117
+ }
118
+
119
+ /** A file name (without `.md`) for an entry name: a slug, never hidden, never a kept name. */
120
+ export function fileStem(name) {
121
+ const slug = safeSlug(name).replace(/^\.+/, '') || 'unnamed';
122
+ if (WINDOWS_DEVICE_NAME.test(slug) || INSTRUCTION_FILE_NAME.test(slug)) {
123
+ return slug.replace(/^[^.]*/, (head) => head + KEPT_NAME_SUFFIX);
124
+ }
125
+ return slug;
126
+ }
127
+
128
+ /** An entry's name: memory_list's `name`, else the domain without its type prefix. */
129
+ function entryName(entry) {
130
+ if (typeof entry.name === 'string' && entry.name.trim()) return entry.name;
131
+ return String(entry.domain || '').replace(/^_[^:]+:/, '');
132
+ }
133
+
134
+ const PLACES = new Set(['agent', 'shared', 'business', 'hidden']);
135
+
136
+ /** memory_list's `placement`, when the builder returned one in the shape it documents. */
137
+ function builderPlacement(entry) {
138
+ const p = entry?.placement;
139
+ if (!p || typeof p !== 'object' || Array.isArray(p) || !PLACES.has(p.where)) return null;
140
+ if (p.where === 'agent' && (typeof p.agent !== 'string' || typeof p.group !== 'string')) return null;
141
+ return p;
142
+ }
143
+
144
+ /** memory_list's `owner`, when the builder returned the field (null is an answer: no owner). */
145
+ function builderOwner(entry) {
146
+ if (!entry || !Object.prototype.hasOwnProperty.call(entry, 'owner')) return { known: false };
147
+ const owner = entry.owner;
148
+ if (owner === null) return { known: true, owner: null };
149
+ if (typeof owner === 'string') return { known: true, owner: owner.trim().toLowerCase() || null };
150
+ return { known: false };
151
+ }
152
+
153
+ /** The Memory page link for an `_account:*` row: memory_list's own, or built like the builder's memoryPageUrl. */
154
+ function accountEditUrl(entry, placement, accountId, appUrl) {
155
+ if (typeof entry.memory_page_url === 'string' && /^https:\/\//.test(entry.memory_page_url)) return entry.memory_page_url;
156
+ const page = accountMemoryDashboardUrl(accountId, appUrl);
157
+ const query = new URLSearchParams();
158
+ if (placement.where === 'agent' && placement.agent === 'orchestrator') {
159
+ query.set('agent', 'orchestrator');
160
+ const item = String(entry.domain || '').trim();
161
+ if (item && item.length <= 60) query.set('item', item);
162
+ } else if (placement.where === 'business' && placement.group === 'voice') {
163
+ query.set('open', 'voice');
164
+ }
165
+ const search = query.toString();
166
+ return search ? `${page}?${search}` : page;
167
+ }
168
+
169
+ /** The slug of an `_account:*` row after its prefixes: `_account:_rule:x` is `x`. */
170
+ function accountSlug(domain) {
171
+ return String(domain || '')
172
+ .trim()
173
+ .replace(/^_account:/i, '')
174
+ .replace(/^(?:_rule:|_skill:|_command:|_agent:|memory:)/i, '');
175
+ }
176
+
177
+ /** Folder, type and name for an `_account:*` row the page shows, or null. */
178
+ function accountTarget(entry, placement) {
179
+ const slug = accountSlug(entry.domain);
180
+ if (placement.where === 'business') {
181
+ return placement.group === 'voice' ? { dir: VOICE_DIR, type: 'memory', name: slug } : null;
182
+ }
183
+ if (placement.where === 'hidden') return null;
184
+ const owner = placement.where === 'shared' ? SHARED_OWNER : ownerFolder(placement.agent);
185
+ switch (placement.group) {
186
+ case 'how-it-works':
187
+ return { dir: `memory/${owner}`, type: 'memory', name: 'how-it-works' };
188
+ case 'background':
189
+ return { dir: `memory/${owner}`, type: 'memory', name: 'background' };
190
+ case 'rules':
191
+ return { dir: `rules/${owner}`, type: 'rule', name: slug };
192
+ case 'skills':
193
+ return { dir: `skills/${owner}`, type: 'skill', name: slug };
194
+ case 'shortcuts':
195
+ return { dir: `commands/${owner}`, type: 'command', name: slug };
196
+ case 'specialists':
197
+ return { dir: `agents/${owner}`, type: 'agent', name: slug };
198
+ case 'profile':
199
+ return { dir: `identity/${owner}`, type: 'identity', name: slug };
200
+ case 'moved':
201
+ return { dir: `memory/${owner}/moved`, type: 'memory', name: slug };
202
+ default:
203
+ return { dir: `memory/${owner}`, type: 'memory', name: slug };
204
+ }
205
+ }
206
+
207
+ function shortHash(text) {
208
+ return createHash('sha256').update(String(text), 'utf8').digest('hex').slice(0, 6);
209
+ }
210
+
211
+ /**
212
+ * Plan every file one pull writes. `entries` is memory_list's `data` (every
213
+ * account-level row, any type). Returns
214
+ * files: [{ rel, body, readOnly, notFollowed, type, owner, domain }] in domain order
215
+ * skipped: { account, system, hidden } rows deliberately not written
216
+ * listed: every domain the listing returned (moved or skipped rows included)
217
+ * Two entries whose names reduce to the same file get distinct files: the
218
+ * later one (in domain order) gets a short hash of its domain.
219
+ */
220
+ export function planKnowledgeFiles(entries, { accountId = null, appUrl } = {}) {
221
+ const skipped = { account: 0, system: 0, hidden: 0 };
222
+ const listed = new Set();
223
+ const files = [];
224
+ const taken = new Set();
225
+ const rows = entries
226
+ .filter((e) => e && typeof e.domain === 'string' && e.domain.trim())
227
+ .sort((a, b) => (a.domain < b.domain ? -1 : a.domain > b.domain ? 1 : 0));
228
+
229
+ for (const entry of rows) {
230
+ listed.add(entry.domain);
231
+ // About your business: written once from account_memory_get (account-memory.mjs).
232
+ if (isAccountMemoryRow(entry.domain)) {
233
+ skipped.account += 1;
234
+ continue;
235
+ }
236
+ const placementFromBuilder = builderPlacement(entry);
237
+ let target;
238
+ let owner;
239
+ let shownUnder = typeof placementFromBuilder?.label === 'string' ? placementFromBuilder.label : null;
240
+ let readOnly = false;
241
+ let editUrl = null;
242
+ let notFollowed = false;
243
+ let pointer = null;
244
+
245
+ if (isAccountScopedRow(entry.domain)) {
246
+ const placement = placementFromBuilder ?? accountPlacement(entry.domain, entry.content);
247
+ target = accountTarget(entry, placement);
248
+ if (!target) {
249
+ skipped.hidden += 1;
250
+ continue;
251
+ }
252
+ owner = placement.where === 'agent' ? placement.agent : null;
253
+ shownUnder ??= accountPlacementLabel(placement);
254
+ readOnly = true;
255
+ editUrl = accountEditUrl(entry, placement, accountId, appUrl);
256
+ if (placement.where === 'agent' && placement.group === 'moved') pointer = movedTopicPointer(entry.domain);
257
+ } else {
258
+ const system = placementFromBuilder
259
+ ? placementFromBuilder.where === 'hidden' && placementFromBuilder.reason === 'system'
260
+ : isSystemRow(entry.domain, entry.content);
261
+ const type = memoryTypeOf(entry.domain);
262
+ if (system || !type) {
263
+ skipped.system += 1;
264
+ continue;
265
+ }
266
+ const fromBuilder = builderOwner(entry);
267
+ owner = fromBuilder.known ? fromBuilder.owner : ownerOf(entry);
268
+ // Hidden for any other reason: filed under its owner, in the folder no agent reads.
269
+ const placement = placementFromBuilder ?? ownedPlacement(entry.domain, owner);
270
+ notFollowed = placement.where === 'hidden';
271
+ if (notFollowed) shownUnder ??= NOT_SHOWN_LABEL;
272
+ const dir = `${TYPE_TO_FOLDER[type]}/${ownerFolder(owner)}`;
273
+ target = { dir: notFollowed ? `${dir}/${NOT_FOLLOWED_DIR}` : dir, type, name: entryName(entry) };
274
+ }
275
+
276
+ let rel = `${target.dir}/${fileStem(target.name)}.md`;
277
+ if (taken.has(rel.toLowerCase())) rel = `${target.dir}/${fileStem(target.name)}-${shortHash(entry.domain)}.md`;
278
+ taken.add(rel.toLowerCase());
279
+
280
+ const frontmatter = toFrontmatter({
281
+ id: entry.id,
282
+ name: entry.name,
283
+ type: target.type,
284
+ domain: entry.domain,
285
+ owner: owner ?? SHARED_OWNER,
286
+ shown_under: shownUnder ?? undefined,
287
+ followed_by: notFollowed ? 'no agent' : undefined,
288
+ read_only: readOnly ? 'true' : undefined,
289
+ edit_url: editUrl ?? undefined,
290
+ memory_page_url: !readOnly && typeof entry.memory_page_url === 'string' ? entry.memory_page_url : undefined,
291
+ project_id: entry.project_id,
292
+ version: entry.version,
293
+ updated_at: entry.updated_at,
294
+ });
295
+ files.push({
296
+ rel,
297
+ body: `${frontmatter}${pointer ? `${pointer}\n\n` : ''}${entry.content ?? ''}`,
298
+ readOnly,
299
+ notFollowed,
300
+ type: target.type,
301
+ owner: owner ?? null,
302
+ domain: entry.domain,
303
+ });
304
+ }
305
+ return { files, skipped, listed };
306
+ }