@pulsarinteractive/geniustasker 0.0.0-stage → 0.1.0-beta.10

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CONTRIBUTING.md +72 -0
  3. package/LICENSE +21 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +76 -2
  6. package/SECURITY.md +14 -0
  7. package/THIRD_PARTY_NOTICES.md +15 -0
  8. package/bin/geniustasker.mjs +6 -0
  9. package/docs/ARCHITECTURE.md +144 -0
  10. package/docs/CHATGPT.md +86 -0
  11. package/docs/CLI.md +444 -0
  12. package/docs/DOCUMENTATION_SYNC.md +144 -0
  13. package/docs/INTEGRATIONS.md +62 -0
  14. package/docs/LIMITS.md +62 -0
  15. package/docs/MEMORY.md +38 -0
  16. package/docs/OPENAI.md +21 -0
  17. package/docs/OPENAI_PUBLICATION.md +22 -0
  18. package/docs/PLANNING.md +79 -0
  19. package/docs/PROJECT_FILTERS.md +77 -0
  20. package/docs/RELEASING.md +23 -0
  21. package/docs/TESTING.md +52 -0
  22. package/docs/WORKFLOWS.md +37 -0
  23. package/examples/codex.toml +3 -0
  24. package/examples/mcp.json +8 -0
  25. package/mcp.json +10 -0
  26. package/package.json +72 -4
  27. package/plugin.json +24 -0
  28. package/remote/tool-catalog.json +1374 -0
  29. package/remote/workflows.json +67 -0
  30. package/skills/geniustasker/SKILL.md +148 -0
  31. package/skills/geniustasker/agents/openai.yaml +4 -0
  32. package/skills/geniustasker/references/collaboration.md +70 -0
  33. package/skills/geniustasker/references/incremental-reads.md +16 -0
  34. package/skills/geniustasker/references/profiles.md +22 -0
  35. package/skills/geniustasker/references/project-reads.md +43 -0
  36. package/skills/geniustasker-documentation/SKILL.md +39 -0
  37. package/skills/geniustasker-documentation/agents/openai.yaml +4 -0
  38. package/skills/geniustasker-documentation/references/structure.md +29 -0
  39. package/skills/geniustasker-graph/SKILL.md +31 -0
  40. package/skills/geniustasker-graph/agents/openai.yaml +4 -0
  41. package/skills/geniustasker-media/SKILL.md +33 -0
  42. package/skills/geniustasker-media/agents/openai.yaml +4 -0
  43. package/skills/geniustasker-memory/SKILL.md +120 -0
  44. package/skills/geniustasker-memory/agents/openai.yaml +4 -0
  45. package/skills/geniustasker-memory/references/knowledge-workflows.md +57 -0
  46. package/skills/geniustasker-planning/SKILL.md +28 -0
  47. package/skills/geniustasker-planning/agents/openai.yaml +4 -0
  48. package/src/changes.mjs +139 -0
  49. package/src/cli.mjs +511 -0
  50. package/src/client.mjs +264 -0
  51. package/src/command-catalog.mjs +569 -0
  52. package/src/commands.mjs +277 -0
  53. package/src/documentation-graph.mjs +251 -0
  54. package/src/documentation-store.mjs +589 -0
  55. package/src/documentation-sync.mjs +517 -0
  56. package/src/documentation.mjs +136 -0
  57. package/src/errors.mjs +65 -0
  58. package/src/knowledge.mjs +109 -0
  59. package/src/login.mjs +258 -0
  60. package/src/mcp.mjs +327 -0
  61. package/src/media.mjs +307 -0
  62. package/src/presentation.d.mts +1 -0
  63. package/src/presentation.mjs +12 -0
  64. package/src/profiles.mjs +154 -0
  65. package/src/read-cache.mjs +111 -0
  66. package/src/store.mjs +141 -0
  67. package/src/transport.mjs +201 -0
  68. package/src/workflows.mjs +9 -0
@@ -0,0 +1,277 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { open } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import * as z from 'zod/v4';
6
+ import catalog from './command-catalog.mjs';
7
+ import { CredentialStore } from './store.mjs';
8
+ import { AgentError, fail, safeError } from './errors.mjs';
9
+
10
+ export const operationIdSchema = z
11
+ .string()
12
+ .regex(/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/);
13
+ const identifier = z
14
+ .string()
15
+ .min(1)
16
+ .max(128)
17
+ // eslint-disable-next-line no-control-regex -- Reject control characters in untrusted input.
18
+ .regex(/^[^\u0000-\u001f]+$/u);
19
+ export const commandSchema = z
20
+ .object({
21
+ operationId: operationIdSchema,
22
+ issuedAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
23
+ scope: z
24
+ .string()
25
+ .max(256)
26
+ // eslint-disable-next-line no-control-regex -- Reject control characters in untrusted input.
27
+ .regex(/^(project|memory):[^\u0000-\u001f]+$/u),
28
+ epoch: identifier,
29
+ id: identifier,
30
+ expectedRevision: z
31
+ .string()
32
+ .regex(/^[1-9][0-9]{0,15}$/)
33
+ .nullable(),
34
+ kind: z.enum(Object.keys(catalog.commands)),
35
+ data: z.record(z.string(), z.json()),
36
+ })
37
+ .strict();
38
+ export const draftSchema = commandSchema.omit({ operationId: true, issuedAt: true });
39
+ const canonical = (value) =>
40
+ JSON.stringify(value, (_, item) =>
41
+ item && typeof item === 'object' && !Array.isArray(item)
42
+ ? Object.fromEntries(
43
+ Object.keys(item)
44
+ .sort()
45
+ .map((key) => [key, item[key]]),
46
+ )
47
+ : item,
48
+ );
49
+ const checked = (schema, value) => {
50
+ const parsed = schema.safeParse(value);
51
+ if (!parsed.success) {
52
+ fail(
53
+ 'invalid_arguments',
54
+ 'The command does not match its schema. Use commands describe; do not supply credentials or server-owned fields.',
55
+ );
56
+ }
57
+ return parsed.data;
58
+ };
59
+ /** Validate the public envelope, generated field allowlist and resource scope. */
60
+ export function validateCommand(value) {
61
+ const command = checked(commandSchema, value),
62
+ definition = catalog.commands[command.kind];
63
+ if (
64
+ Buffer.byteLength(JSON.stringify(command)) > catalog.maxCommandBytes ||
65
+ Object.keys(command.data).some((key) => !definition.fields.includes(key)) ||
66
+ (definition.create ? command.expectedRevision !== null : command.expectedRevision === null)
67
+ ) {
68
+ fail(
69
+ 'invalid_arguments',
70
+ 'Use the declared command fields and size limit. Creation requires a null revision; other commands require the current record revision.',
71
+ );
72
+ }
73
+ const prefix = definition.module.startsWith('Memory/') ? 'memory:' : 'project:';
74
+ if (
75
+ !command.scope.startsWith(prefix) ||
76
+ (definition.module.endsWith('/$') && command.id !== command.scope.slice(prefix.length))
77
+ ) {
78
+ fail('invalid_arguments', 'The command must target the matching project or memory scope.');
79
+ }
80
+ if (
81
+ ['memory.create', 'project.create'].includes(command.kind) &&
82
+ (!/^[a-f\d]{8}-[a-f\d]{4}-4[a-f\d]{3}-[89ab][a-f\d]{3}-[a-f\d]{12}$/i.test(command.id) ||
83
+ command.epoch !== command.id)
84
+ ) {
85
+ fail(
86
+ 'invalid_arguments',
87
+ 'Create a space with a UUID v4 as id and the same initial epoch. Use project:<id> or memory:<id> as appropriate; keep source identifiers in page sourceId.',
88
+ );
89
+ }
90
+ if (command.kind === 'ticket.plan' && command.id !== command.scope.slice(8)) {
91
+ fail(
92
+ 'invalid_arguments',
93
+ 'ticket.plan targets the project ID and root revision; changes carry each task expectedVersion.',
94
+ );
95
+ }
96
+ return command;
97
+ }
98
+ export function describeCommands() {
99
+ return {
100
+ ...catalog,
101
+ envelope: z.toJSONSchema(commandSchema),
102
+ draft: z.toJSONSchema(draftSchema),
103
+ recovery:
104
+ 'Retain operationId, issuedAt and the complete body. Retry an uncertain write unchanged; reread before creating a new edit after a conflict.',
105
+ };
106
+ }
107
+ /** Read one bounded regular JSON file, rejecting symlinks and non-file inputs. */
108
+ export async function readCommandFile(path) {
109
+ if (typeof path !== 'string') {
110
+ fail('invalid_arguments', 'Supply --file with a JSON command or draft.');
111
+ }
112
+ let handle;
113
+ try {
114
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
115
+ const stat = await handle.stat();
116
+ if (!stat.isFile() || stat.size > catalog.maxCommandBytes) {
117
+ fail(
118
+ 'invalid_arguments',
119
+ `Use a regular JSON file no larger than ${catalog.maxCommandBytes} bytes.`,
120
+ );
121
+ }
122
+ const bytes = Buffer.alloc(catalog.maxCommandBytes + 1),
123
+ result = await handle.read(bytes, 0, bytes.length, 0);
124
+ if (result.bytesRead > catalog.maxCommandBytes) {
125
+ fail('invalid_arguments', `Command file exceeds ${catalog.maxCommandBytes} bytes.`);
126
+ }
127
+ return JSON.parse(bytes.subarray(0, result.bytesRead).toString('utf8'));
128
+ } catch (error) {
129
+ if (error instanceof AgentError) {
130
+ throw error;
131
+ }
132
+ fail('invalid_arguments', 'Could not read a regular, valid JSON command file.');
133
+ } finally {
134
+ await handle?.close();
135
+ }
136
+ }
137
+ const binding = (session) => ({
138
+ origin: session.origin,
139
+ connectionId: session.connection_id,
140
+ profileId: session.profile_id,
141
+ sessionId: session.session_id,
142
+ });
143
+ const receiptSchema = z
144
+ .object({
145
+ operationId: operationIdSchema,
146
+ scope: z.string(),
147
+ kind: z.string(),
148
+ epoch: z.string(),
149
+ cursor: z.string(),
150
+ items: z.array(
151
+ z
152
+ .object({
153
+ id: z.string(),
154
+ revision: z.string(),
155
+ collection: z.string(),
156
+ deleted: z.boolean(),
157
+ })
158
+ .strict(),
159
+ ),
160
+ affectedCollections: z.array(z.string()),
161
+ })
162
+ .strict();
163
+
164
+ /** A journal is bound to one authenticated terminal identity. Persist the full
165
+ * request before the network call, never regenerate a UUID after a timeout.
166
+ * It contains private project text but no access/refresh tokens. A process crash
167
+ * leaves its operation lock for explicit recovery after all clients stop. */
168
+ export class CommandJournal {
169
+ constructor(client) {
170
+ this.client = client;
171
+ }
172
+ store(id) {
173
+ checked(operationIdSchema, id);
174
+ return new CredentialStore({
175
+ directory: join(this.client.store.directory, 'operations', this.client.store.context),
176
+ context: id,
177
+ maxBytes: 524288,
178
+ });
179
+ }
180
+ async prepare(draft) {
181
+ const input = validateCommand({
182
+ ...checked(draftSchema, draft),
183
+ operationId: randomUUID(),
184
+ issuedAt: Date.now(),
185
+ });
186
+ const identity = binding(await this.client.session()),
187
+ store = this.store(input.operationId);
188
+ await store.locked(
189
+ () => store.write({ formatVersion: 1, binding: identity, input, state: 'prepared' }),
190
+ this.client.transport.signal,
191
+ );
192
+ return { operationId: input.operationId, state: 'prepared', request: input };
193
+ }
194
+ async inspect(id) {
195
+ const row = await this.store(id).read();
196
+ if (!row || row.formatVersion !== 1 || row.input?.operationId !== id) {
197
+ fail('operation_not_found', 'This context has no valid local operation with that ID.');
198
+ }
199
+ return {
200
+ operationId: id,
201
+ source: 'local_journal',
202
+ state: row.state,
203
+ request: row.input,
204
+ ...(row.receipt ? { receipt: row.receipt } : {}),
205
+ ...(row.error ? { error: row.error } : {}),
206
+ };
207
+ }
208
+ async execute(value) {
209
+ const input = validateCommand(value),
210
+ store = this.store(input.operationId);
211
+ return store.locked(async () => {
212
+ // Capture token and identity together under the refresh lock. A later
213
+ // logout/login cannot turn this pending operation into another profile.
214
+ const session = await this.client.authorizedSession(),
215
+ identity = binding(session);
216
+ let row = await store.read();
217
+ if (row && (row.formatVersion !== 1 || canonical(row.binding) !== canonical(identity))) {
218
+ fail(
219
+ 'operation_identity_mismatch',
220
+ 'This operation belongs to another connection, profile, session or environment.',
221
+ {
222
+ remediation:
223
+ 'Inspect it locally and reread current records. Do not replay it through a replacement login.',
224
+ },
225
+ );
226
+ }
227
+ if (row && canonical(row.input) !== canonical(input)) {
228
+ fail(
229
+ 'idempotency_mismatch',
230
+ 'The operation ID already belongs to a different immutable request.',
231
+ { status: 409, remediation: 'Use commands inspect, then retry the saved operation.' },
232
+ );
233
+ }
234
+ if (row?.state === 'confirmed') {
235
+ return { source: 'local_receipt', receipt: row.receipt };
236
+ }
237
+ row = { formatVersion: 1, binding: identity, input, state: 'uncertain' };
238
+ await store.write(row);
239
+ try {
240
+ const receipt = await this.client.transport.request('/api/agents/v1/commands', {
241
+ method: 'POST',
242
+ json: input,
243
+ token: session.access_token,
244
+ });
245
+ if (
246
+ !receiptSchema.safeParse(receipt).success ||
247
+ ['operationId', 'scope', 'kind', 'epoch'].some((key) => receipt[key] !== input[key])
248
+ ) {
249
+ fail(
250
+ 'invalid_response',
251
+ 'The command receipt was invalid. The server may have committed.',
252
+ { execution: 'unknown', remediation: 'Retry the identical saved operation.' },
253
+ );
254
+ }
255
+ await store.write({ ...row, state: 'confirmed', receipt });
256
+ return { source: 'server', receipt };
257
+ } catch (error) {
258
+ const safe = safeError(error);
259
+ const result = new AgentError(safe.code, safe.message, {
260
+ status: safe.status,
261
+ retryable: safe.retryable,
262
+ remediation: safe.remediation,
263
+ execution: 'unknown',
264
+ ...safe.details,
265
+ operationId: input.operationId,
266
+ });
267
+ // The uncertainty marker is already durable. Preserve the useful original
268
+ // error/operation ID even if disk pressure prevents saving diagnostics.
269
+ await store.write({ ...row, error: result.toJSON() }).catch(() => {});
270
+ throw result;
271
+ }
272
+ }, this.client.transport.signal);
273
+ }
274
+ async retry(id) {
275
+ return this.execute((await this.inspect(id)).request);
276
+ }
277
+ }
@@ -0,0 +1,251 @@
1
+ import * as z from 'zod/v4';
2
+ import { readChanges } from './changes.mjs';
3
+ import { fail } from './errors.mjs';
4
+ const id = z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/);
5
+ const recordSchema = z.object({
6
+ id,
7
+ revision: z.string().regex(/^[0-9]+$/),
8
+ data: z.record(z.string(), z.unknown()),
9
+ });
10
+ const parse = (schema, value) => {
11
+ const result = schema.safeParse(value);
12
+ if (!result.success) {
13
+ fail('invalid_response', 'The Graph source format is invalid.');
14
+ }
15
+ return result.data;
16
+ };
17
+ const contextOf = (tree) => ({ epoch: tree.epoch, checkpoint: tree.checkpoint });
18
+ const equivalentAccess = (before, tree) =>
19
+ before?.epoch === tree.epoch && before.accessRevision === tree.accessRevision;
20
+ const escape = (value) =>
21
+ String(value)
22
+ .replace(/[\r\n]/g, ' ')
23
+ .replace(/[\\`*_[\]<>|]/g, '\\$&');
24
+
25
+ async function graphCollection(client, scope, collection, tree, previous, cursor, downloads) {
26
+ const records = new Map((previous ?? []).map((record) => [record.id, record]));
27
+ if (cursor) {
28
+ try {
29
+ for (let count = 0; count < 1000; count++) {
30
+ const page = await readChanges(client, { scope, collection, cursor });
31
+ if (page.epoch !== tree.epoch || page.omittedIds.length) {
32
+ fail('incomplete_documentation', 'The Graph change response is incomplete.');
33
+ }
34
+ for (const item of page.items) {
35
+ if (item.kind === 'remove') {
36
+ records.delete(item.id);
37
+ } else {
38
+ records.set(
39
+ item.id,
40
+ parse(recordSchema, { id: item.id, revision: item.revision, data: item.data }),
41
+ );
42
+ }
43
+ }
44
+ cursor = page.nextCursor;
45
+ if (!page.hasMore) {
46
+ return { items: [...records.values()], cursor };
47
+ }
48
+ }
49
+ fail('documentation_capacity', 'The Graph change window exceeds its request budget.');
50
+ } catch (error) {
51
+ if (!['reset_required', 'agent_change_history_expired'].includes(error.code)) {
52
+ throw error;
53
+ }
54
+ records.clear(); // Explicit server reset: replace from a fresh bounded inventory.
55
+ }
56
+ }
57
+ let after;
58
+ for (let count = 0; count < 1000; count++) {
59
+ const query = { scope, collection, after, ...contextOf(tree) };
60
+ const key = JSON.stringify(query),
61
+ bucket = downloads.graphBucket(tree);
62
+ const cached = await downloads.read(scope, bucket, key, 8388608);
63
+ const page = cached
64
+ ? JSON.parse(cached.toString('utf8'))
65
+ : await client.get('resources', query);
66
+ if (
67
+ page.scope !== scope ||
68
+ page.collection !== collection ||
69
+ page.epoch !== tree.epoch ||
70
+ page.checkpoint !== tree.checkpoint ||
71
+ !Array.isArray(page.items) ||
72
+ page.items.length > 50 ||
73
+ !Array.isArray(page.omittedIds) ||
74
+ page.omittedIds.length
75
+ ) {
76
+ fail('incomplete_documentation', 'The Graph inventory is incomplete.');
77
+ }
78
+ for (const item of page.items) {
79
+ const record = parse(recordSchema, item);
80
+ if (records.has(record.id)) {
81
+ fail('invalid_response', 'The Graph inventory repeated an item.');
82
+ }
83
+ records.set(record.id, record);
84
+ }
85
+ if (!cached) {
86
+ await downloads.put(scope, bucket, key, Buffer.from(JSON.stringify(page)));
87
+ }
88
+ if (!page.nextAfter) {
89
+ if (!page.changeCursor) {
90
+ fail('invalid_response', 'The Graph inventory has no incremental cursor.');
91
+ }
92
+ return { items: [...records.values()], cursor: page.changeCursor };
93
+ }
94
+ if (page.nextAfter === after || page.nextAfter !== page.items.at(-1)?.id) {
95
+ fail('invalid_response', 'The Graph inventory cursor did not advance.');
96
+ }
97
+ after = page.nextAfter;
98
+ }
99
+ fail('documentation_capacity', 'The Graph inventory exceeds its request budget.');
100
+ }
101
+
102
+ export async function exportGraph(client, store, source, tree, previous, state, assets, downloads) {
103
+ if (!source.graph) {
104
+ return;
105
+ }
106
+ const file = `${source.scope.replace(':', '-')}/graph.json`;
107
+ const oldBytes =
108
+ previous?.graph && equivalentAccess(previous, tree)
109
+ ? await store.readPrevious(previous.graph.file)
110
+ : null;
111
+ const oldIndex = oldBytes ? JSON.parse(oldBytes.toString('utf8')) : null;
112
+ let old = oldIndex;
113
+ if (oldIndex?.formatVersion === 2) {
114
+ old = { nodes: [], edges: [] };
115
+ for (const collection of ['nodes', 'edges']) {
116
+ const chunks = oldIndex.chunks?.[collection];
117
+ if (!Array.isArray(chunks) || chunks.length > 300) {
118
+ fail('invalid_documentation_manifest', 'The Graph chunk index is invalid.');
119
+ }
120
+ for (const path of chunks) {
121
+ const bytes = await store.readPrevious(path);
122
+ if (!bytes) {
123
+ fail('invalid_documentation_manifest', 'A tracked Graph chunk is missing.');
124
+ }
125
+ const chunk = JSON.parse(bytes.toString('utf8'));
126
+ if (!Array.isArray(chunk) || chunk.length > 100) {
127
+ fail('invalid_documentation_manifest', 'A Graph chunk exceeds its bound.');
128
+ }
129
+ old[collection].push(...chunk.map((record) => parse(recordSchema, record)));
130
+ }
131
+ }
132
+ }
133
+ const nodes = await graphCollection(
134
+ client,
135
+ source.scope,
136
+ 'nodes',
137
+ tree,
138
+ old?.nodes,
139
+ old && previous.graph.nodesCursor,
140
+ downloads,
141
+ );
142
+ const edges = await graphCollection(
143
+ client,
144
+ source.scope,
145
+ 'edges',
146
+ tree,
147
+ old?.edges,
148
+ old && previous.graph.edgesCursor,
149
+ downloads,
150
+ );
151
+ if (nodes.items.length > 10000 || edges.items.length > 30000) {
152
+ fail('documentation_capacity', 'The Graph exceeds its source capacity.');
153
+ }
154
+ const ids = new Set(nodes.items.map((node) => node.id));
155
+ if (edges.items.some((edge) => !ids.has(edge.data.source) || !ids.has(edge.data.target))) {
156
+ fail(
157
+ 'read_context_changed',
158
+ 'The Graph has missing endpoints; refresh its source before exporting.',
159
+ );
160
+ }
161
+ for (const node of nodes.items) {
162
+ if (node.data.imageAssetId) {
163
+ assets.add(parse(id, node.data.imageAssetId));
164
+ }
165
+ }
166
+ // Bounded portable chunks keep large Graphs out of one enormous JSON/Markdown
167
+ // file. Borrowed references remain identities, never cached source bodies.
168
+ const notice =
169
+ '> Automatically generated from GeniusTasker. Edit through the [MCP](https://github.com/PulsarInteractive/geniustasker-agent-tools/blob/main/docs/DOCUMENTATION_SYNC.md), then sync again.';
170
+ const footer = `Source scope: \`${source.scope}\` · Epoch: \`${tree.epoch}\` · Checkpoint: \`${tree.checkpoint}\``;
171
+ const overview = [
172
+ notice,
173
+ '',
174
+ `# ${escape(tree.title)} — Graph`,
175
+ '',
176
+ 'Linked Memory content is resolved online with current permissions. These files preserve this Graph’s own notes and qualified relations.',
177
+ '',
178
+ ];
179
+ const chunks = { nodes: [], edges: [] };
180
+ for (const [collection, records] of [
181
+ ['nodes', nodes.items],
182
+ ['edges', edges.items],
183
+ ]) {
184
+ records.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
185
+ for (let offset = 0; offset < records.length; offset += 100) {
186
+ const part = records.slice(offset, offset + 100),
187
+ name = `graph/${collection}-${String(offset / 100).padStart(4, '0')}`;
188
+ const path = `${source.scope.replace(':', '-')}/${name}`;
189
+ chunks[collection].push(path + '.json');
190
+ await store.put(path + '.json', JSON.stringify(part, null, 2) + '\n');
191
+ const text = [
192
+ notice,
193
+ '',
194
+ `# ${collection === 'nodes' ? 'Nodes' : 'Relations'} ${offset + 1}–${offset + part.length}`,
195
+ '',
196
+ ];
197
+ for (const record of part) {
198
+ if (collection === 'nodes') {
199
+ text.push(
200
+ `## ${escape(record.data.title ?? record.id)}`,
201
+ '',
202
+ String(record.data.text ?? record.data.summary ?? ''),
203
+ '',
204
+ );
205
+ for (const ref of record.data.references ?? []) {
206
+ text.push(
207
+ `- Memory reference: \`${escape(ref.scope)}\` / \`${escape(ref.pageId)}\`${ref.blockId ? ' / `' + escape(ref.blockId) + '`' : ''} (resolve current access)`,
208
+ );
209
+ }
210
+ } else {
211
+ text.push(
212
+ `## ${escape(record.data.label || record.data.relation)}`,
213
+ '',
214
+ `\`${escape(record.data.source)}\` → \`${escape(record.data.target)}\` · ${escape(record.data.certainty)}`,
215
+ '',
216
+ String(record.data.note ?? ''),
217
+ '',
218
+ );
219
+ }
220
+ text.push(
221
+ `Source ID: \`${record.id}\` · Revision: \`${record.revision}\` · Updated: ${escape(record.data.updated ?? 'unknown')} · Author: ${escape(record.data.lastEditor ?? record.data.creator ?? 'unknown')}`,
222
+ '',
223
+ );
224
+ }
225
+ text.push('', '---', '', footer, '');
226
+ await store.put(path + '.md', text.join('\n'));
227
+ overview.push(
228
+ `- [${collection === 'nodes' ? 'Nodes' : 'Relations'} ${offset + 1}–${offset + part.length}](${name}.md) · [JSON](${name}.json)`,
229
+ );
230
+ }
231
+ }
232
+ overview.push('', '---', '', footer, '');
233
+ await store.put(file.replace('.json', '.md'), overview.join('\n'));
234
+ await store.put(
235
+ file,
236
+ JSON.stringify(
237
+ {
238
+ formatVersion: 2,
239
+ scope: source.scope,
240
+ epoch: tree.epoch,
241
+ counts: { nodes: nodes.items.length, edges: edges.items.length },
242
+ chunks,
243
+ referencePolicy:
244
+ 'Resolve linked Memory content online with current access. Unavailable targets do not remove nodes.',
245
+ },
246
+ null,
247
+ 2,
248
+ ) + '\n',
249
+ );
250
+ state.graph = { file, nodesCursor: nodes.cursor, edgesCursor: edges.cursor };
251
+ }