@pulsarinteractive/geniustasker 0.1.0-beta.4 → 0.1.0-beta.6
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/CHANGELOG.md +20 -0
- package/README.md +10 -9
- package/docs/ARCHITECTURE.md +18 -0
- package/docs/CLI.md +30 -5
- package/docs/DOCUMENTATION_SYNC.md +144 -0
- package/docs/INTEGRATIONS.md +2 -2
- package/docs/LIMITS.md +15 -2
- package/docs/PROJECT_FILTERS.md +77 -0
- package/examples/codex.toml +1 -1
- package/examples/mcp.json +1 -1
- package/mcp.json +1 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/skills/geniustasker/SKILL.md +16 -0
- package/skills/geniustasker-documentation/SKILL.md +39 -0
- package/skills/geniustasker-documentation/agents/openai.yaml +4 -0
- package/skills/geniustasker-documentation/references/structure.md +29 -0
- package/skills/geniustasker-graph/SKILL.md +31 -0
- package/skills/geniustasker-graph/agents/openai.yaml +4 -0
- package/skills/geniustasker-media/SKILL.md +33 -0
- package/skills/geniustasker-media/agents/openai.yaml +4 -0
- package/skills/geniustasker-memory/SKILL.md +9 -2
- package/src/cli.mjs +78 -1
- package/src/client.mjs +2 -0
- package/src/command-catalog.mjs +48 -0
- package/src/documentation-graph.mjs +251 -0
- package/src/documentation-store.mjs +589 -0
- package/src/documentation-sync.mjs +517 -0
- package/src/documentation.mjs +136 -0
- package/src/mcp.mjs +47 -1
- package/src/media.mjs +65 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geniustasker-media
|
|
3
|
+
description: Compare, reuse, upload and link GeniusTasker Memory resources with stable asset IDs. Use when maintaining document images, attachments or Graph thumbnails, especially when replacing an existing visual.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory resources
|
|
7
|
+
|
|
8
|
+
Discover `mediaUploadAvailable`, the current scope and the necessary media
|
|
9
|
+
permissions. Work only with explicitly selected files and destinations.
|
|
10
|
+
|
|
11
|
+
Before replacing an existing resource, call `tasker_media_compare` with the
|
|
12
|
+
local file, scope and asset ID. Equal bytes should reuse that ID without an upload
|
|
13
|
+
or new document revision. When bytes differ, inspect the old and new image or
|
|
14
|
+
media: a different checksum does not establish a visual improvement. Explain the
|
|
15
|
+
actual change before describing it as improved.
|
|
16
|
+
|
|
17
|
+
Prepare authorized uploads with `tasker_media_prepare`; retain the operation ID
|
|
18
|
+
and use `tasker_media_upload`. Retry uncertain transfers with that same ID.
|
|
19
|
+
Link the returned asset ID from document blocks or node thumbnails; never embed
|
|
20
|
+
base64 or public bucket URLs. One asset can have many uses without duplicate
|
|
21
|
+
storage. Asset bytes belong to the owning Memory’s quota.
|
|
22
|
+
|
|
23
|
+
Changing an image means publishing a new immutable resource and updating the
|
|
24
|
+
intended references with their current revisions. Reuse the old resource for any
|
|
25
|
+
pages that should keep it. When `protectedMemoryAssets` is advertised, removal
|
|
26
|
+
of an in-use resource returns `memory_asset_in_use`, including hidden uses.
|
|
27
|
+
Do not probe private page names or remove other users’ links to bypass the guard.
|
|
28
|
+
After the last authorized use is removed, normal resource removal can proceed.
|
|
29
|
+
|
|
30
|
+
After relevant edits, sync any configured repository documentation. Its optional
|
|
31
|
+
local media are derived snapshots; do not hand-edit them and claim the source was
|
|
32
|
+
updated. Report actual IDs, changed references and verification, without exposing
|
|
33
|
+
unrelated private source content.
|
|
@@ -13,7 +13,7 @@ Do not collect passwords or tokens, approve your own consent, or edit credential
|
|
|
13
13
|
|
|
14
14
|
1. Call `tasker_identity` and `tasker_capabilities` (CLI `whoami`, `capabilities`).
|
|
15
15
|
2. Use `tasker_memories` / `memories list`. Follow `nextAfter`, including empty pages.
|
|
16
|
-
3.
|
|
16
|
+
3. When `documentationSync` is advertised, use `tasker_memory_tree` to navigate folders and compare branch versions. Otherwise read `tasker_resources` with `collection: pageIndex`.
|
|
17
17
|
4. Fetch the needed `pages` by `id` or `path`. Targeted reads do not return a change cursor.
|
|
18
18
|
5. Use `tasker_search` (`memory search`) to find page or node excerpts. Read the
|
|
19
19
|
full record before editing; an excerpt does not establish the full context.
|
|
@@ -83,7 +83,10 @@ type; retain the operation ID and pass it to `tasker_media_upload`. After a time
|
|
|
83
83
|
retry that same ID. Link its returned assetId from an image or attachment block.
|
|
84
84
|
Do not embed base64 files in documents. Use an explicit new destination for
|
|
85
85
|
`tasker_media_download`; never upload a directory or private administrative files.
|
|
86
|
-
The Memory owner pays for the bytes.
|
|
86
|
+
The Memory owner pays for the bytes. Compare proposed replacements with
|
|
87
|
+
`tasker_media_compare` and reuse an identical asset ID. When
|
|
88
|
+
`protectedMemoryAssets` is advertised, in-use resource deletion is blocked,
|
|
89
|
+
including private-page and Graph uses; unlink only the authorized references.
|
|
87
90
|
Image/PDF/text support and byte limits come from capabilities, not guesswork.
|
|
88
91
|
Report what was actually stored, verified, skipped and still pending without
|
|
89
92
|
exposing private source content.
|
|
@@ -111,3 +114,7 @@ shows every type and supports type/status filters. Prefer `auto`; an explicit
|
|
|
111
114
|
Upload only approved imagery. Attaching a private image to a shared Graph makes
|
|
112
115
|
it visible to that Graph's readers and requires the user's authorization.
|
|
113
116
|
Read the full current record before editing and preserve its revision.
|
|
117
|
+
|
|
118
|
+
For a repository documentation mirror, use the documentation skill and packaged
|
|
119
|
+
`docs/DOCUMENTATION_SYNC.md`: check at least once per working day before relying
|
|
120
|
+
on the local copy, and sync again after relevant completed changes.
|
package/src/cli.mjs
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import {
|
|
3
|
+
syncDocumentation,
|
|
4
|
+
documentationStatus,
|
|
5
|
+
unlockDocumentation,
|
|
6
|
+
} from './documentation-sync.mjs';
|
|
1
7
|
import open, { apps } from 'open';
|
|
2
8
|
import catalog from './command-catalog.mjs';
|
|
3
9
|
import { CredentialStore } from './store.mjs';
|
|
@@ -7,8 +13,9 @@ import { deviceLogin, browserLogin } from './login.mjs';
|
|
|
7
13
|
import { CommandJournal, describeCommands, readCommandFile } from './commands.mjs';
|
|
8
14
|
import { AgentProfiles } from './profiles.mjs';
|
|
9
15
|
import { readChanges, watchChanges } from './changes.mjs';
|
|
16
|
+
import { readMemoryTree, readMemoryMarkdown } from './documentation.mjs';
|
|
10
17
|
import { searchMemory, exploreGraph, resolveMemory } from './knowledge.mjs';
|
|
11
|
-
import { MediaJournal, mediaAction, downloadMedia } from './media.mjs';
|
|
18
|
+
import { MediaJournal, mediaAction, downloadMedia, compareMedia } from './media.mjs';
|
|
12
19
|
import { fail, safeError, exitCode } from './errors.mjs';
|
|
13
20
|
|
|
14
21
|
export const commands = {
|
|
@@ -49,6 +56,10 @@ export const commands = {
|
|
|
49
56
|
summary: 'List independent account memories; follow nextAfter even for an empty page.',
|
|
50
57
|
options: ['after'],
|
|
51
58
|
},
|
|
59
|
+
'media compare': {
|
|
60
|
+
summary: 'Compare a local file with an existing resource before uploading a replacement.',
|
|
61
|
+
options: ['scope', 'id', 'file'],
|
|
62
|
+
},
|
|
52
63
|
'media prepare': {
|
|
53
64
|
summary: 'Prepare a local private attachment; retain its operation ID.',
|
|
54
65
|
options: ['scope', 'file', 'content-type', 'epoch'],
|
|
@@ -65,6 +76,26 @@ export const commands = {
|
|
|
65
76
|
summary: 'Download an authorized attachment to a new local file.',
|
|
66
77
|
options: ['scope', 'owner', 'id', 'file'],
|
|
67
78
|
},
|
|
79
|
+
'docs sync': {
|
|
80
|
+
summary: 'Refresh selected Memory/Graph sources in a managed local documentation directory.',
|
|
81
|
+
options: ['directory', 'file', 'max-files', 'max-bytes'],
|
|
82
|
+
},
|
|
83
|
+
'docs status': {
|
|
84
|
+
summary: 'Inspect the local snapshot date and selections without a network request.',
|
|
85
|
+
options: ['directory'],
|
|
86
|
+
},
|
|
87
|
+
'docs unlock': {
|
|
88
|
+
summary: 'Remove a sync lock only when its process on this host has stopped.',
|
|
89
|
+
options: ['directory'],
|
|
90
|
+
},
|
|
91
|
+
'memory tree': {
|
|
92
|
+
summary: 'Read one folder and descendant versions; unchanged validators avoid page bodies.',
|
|
93
|
+
options: ['scope', 'parent', 'after', 'epoch', 'checkpoint', 'if-none-match'],
|
|
94
|
+
},
|
|
95
|
+
'memory markdown': {
|
|
96
|
+
summary: 'Fetch one portable Markdown document with stable media IDs and source metadata.',
|
|
97
|
+
options: ['scope', 'page', 'revision', 'epoch', 'checkpoint'],
|
|
98
|
+
},
|
|
68
99
|
'memory search': {
|
|
69
100
|
summary: 'Search document blocks or Graph nodes without loading the corpus.',
|
|
70
101
|
options: ['scope', 'query', 'collection', 'after', 'epoch', 'checkpoint'],
|
|
@@ -343,6 +374,52 @@ export async function main(
|
|
|
343
374
|
? {}
|
|
344
375
|
: { waitSeconds: Number(options['wait-seconds']) }),
|
|
345
376
|
});
|
|
377
|
+
} else if (command === 'docs sync') {
|
|
378
|
+
let configuration;
|
|
379
|
+
if (options.file) {
|
|
380
|
+
const raw = await readFile(options.file, 'utf8');
|
|
381
|
+
if (Buffer.byteLength(raw) > 32768) {
|
|
382
|
+
fail('invalid_arguments', 'The source configuration is too large.');
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
configuration = JSON.parse(raw);
|
|
386
|
+
} catch {
|
|
387
|
+
fail('invalid_arguments', 'The source configuration must be JSON.');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
data = await syncDocumentation(client, {
|
|
391
|
+
directory: options.directory,
|
|
392
|
+
configuration,
|
|
393
|
+
maxFiles: options['max-files'] === undefined ? undefined : Number(options['max-files']),
|
|
394
|
+
maxBytes: options['max-bytes'] === undefined ? undefined : Number(options['max-bytes']),
|
|
395
|
+
});
|
|
396
|
+
} else if (command === 'docs status' || command === 'docs unlock') {
|
|
397
|
+
data = await (command === 'docs status' ? documentationStatus : unlockDocumentation)({
|
|
398
|
+
directory: options.directory,
|
|
399
|
+
});
|
|
400
|
+
} else if (command === 'memory tree') {
|
|
401
|
+
data = await readMemoryTree(client, {
|
|
402
|
+
scope: options.scope,
|
|
403
|
+
parentId: options.parent,
|
|
404
|
+
after: options.after,
|
|
405
|
+
epoch: options.epoch,
|
|
406
|
+
checkpoint: options.checkpoint,
|
|
407
|
+
ifNoneMatch: options['if-none-match'],
|
|
408
|
+
});
|
|
409
|
+
} else if (command === 'memory markdown') {
|
|
410
|
+
data = await readMemoryMarkdown(client, {
|
|
411
|
+
scope: options.scope,
|
|
412
|
+
pageId: options.page,
|
|
413
|
+
revision: options.revision,
|
|
414
|
+
epoch: options.epoch,
|
|
415
|
+
checkpoint: options.checkpoint,
|
|
416
|
+
});
|
|
417
|
+
} else if (command === 'media compare') {
|
|
418
|
+
data = await compareMedia(client, {
|
|
419
|
+
scope: options.scope,
|
|
420
|
+
id: options.id,
|
|
421
|
+
file: options.file,
|
|
422
|
+
});
|
|
346
423
|
} else if (command === 'media prepare') {
|
|
347
424
|
data = await new MediaJournal(client).prepare({
|
|
348
425
|
scope: options.scope,
|
package/src/client.mjs
CHANGED
package/src/command-catalog.mjs
CHANGED
|
@@ -55,6 +55,29 @@ export default {
|
|
|
55
55
|
"module": "Project/ProjectTaskComment",
|
|
56
56
|
"permission": "comments:write"
|
|
57
57
|
},
|
|
58
|
+
"filter.create": {
|
|
59
|
+
"create": true,
|
|
60
|
+
"fields": [
|
|
61
|
+
"name",
|
|
62
|
+
"criteria"
|
|
63
|
+
],
|
|
64
|
+
"module": "Project/ProjectTaskFilter",
|
|
65
|
+
"permission": "tasks:write"
|
|
66
|
+
},
|
|
67
|
+
"filter.delete": {
|
|
68
|
+
"delete": true,
|
|
69
|
+
"fields": [],
|
|
70
|
+
"module": "Project/ProjectTaskFilter",
|
|
71
|
+
"permission": "tasks:write"
|
|
72
|
+
},
|
|
73
|
+
"filter.update": {
|
|
74
|
+
"fields": [
|
|
75
|
+
"name",
|
|
76
|
+
"criteria"
|
|
77
|
+
],
|
|
78
|
+
"module": "Project/ProjectTaskFilter",
|
|
79
|
+
"permission": "tasks:write"
|
|
80
|
+
},
|
|
58
81
|
"goal.create": {
|
|
59
82
|
"create": true,
|
|
60
83
|
"fields": [
|
|
@@ -279,6 +302,31 @@ export default {
|
|
|
279
302
|
"module": "$private/VoteDraft",
|
|
280
303
|
"permission": "questions:write"
|
|
281
304
|
},
|
|
305
|
+
"tag.create": {
|
|
306
|
+
"create": true,
|
|
307
|
+
"fields": [
|
|
308
|
+
"name",
|
|
309
|
+
"backgroundColor",
|
|
310
|
+
"textColor"
|
|
311
|
+
],
|
|
312
|
+
"module": "Project/ProjectCustomTag",
|
|
313
|
+
"permission": "tasks:write"
|
|
314
|
+
},
|
|
315
|
+
"tag.delete": {
|
|
316
|
+
"delete": true,
|
|
317
|
+
"fields": [],
|
|
318
|
+
"module": "Project/ProjectCustomTag",
|
|
319
|
+
"permission": "tasks:write"
|
|
320
|
+
},
|
|
321
|
+
"tag.update": {
|
|
322
|
+
"fields": [
|
|
323
|
+
"name",
|
|
324
|
+
"backgroundColor",
|
|
325
|
+
"textColor"
|
|
326
|
+
],
|
|
327
|
+
"module": "Project/ProjectCustomTag",
|
|
328
|
+
"permission": "tasks:write"
|
|
329
|
+
},
|
|
282
330
|
"task.archive": {
|
|
283
331
|
"fields": [],
|
|
284
332
|
"module": "Project/ProjectTask",
|
|
@@ -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
|
+
}
|