@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/README.md +183 -9
- package/bin/hiveku-sync.mjs +178 -28
- package/package.json +1 -1
- package/src/claude-md.mjs +193 -0
- package/src/codex-prompt-tools.json +2 -1
- package/src/codex.mjs +234 -45
- package/src/init.mjs +60 -53
- package/src/knowledge-files.mjs +306 -0
- package/src/memory-owner.mjs +464 -0
- package/src/projects.mjs +126 -69
- package/src/pull.mjs +182 -78
- package/src/pullRecord.mjs +98 -0
- package/src/pullTree.mjs +211 -0
- package/src/push.mjs +230 -94
- package/src/pushGuards.mjs +100 -0
- package/src/saveFiles.mjs +111 -0
- package/src/util.mjs +0 -10
- package/src/versionName.mjs +569 -0
- package/src/versions.mjs +811 -0
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,
|
|
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
|
-
|
|
66
|
+
${claudeMdMemoryRegion()}
|
|
62
67
|
|
|
63
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
117
|
-
hiveku-sync push -m "
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
+
}
|