@pulsarinteractive/geniustasker 0.1.0-beta.4 → 0.1.0-beta.5
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 +12 -0
- package/README.md +10 -9
- package/docs/ARCHITECTURE.md +18 -0
- package/docs/CLI.md +1 -1
- package/docs/DOCUMENTATION_SYNC.md +144 -0
- package/docs/INTEGRATIONS.md +2 -2
- package/examples/codex.toml +1 -1
- package/examples/mcp.json +1 -1
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- 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/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,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
|
+
}
|