@pulsarinteractive/geniustasker 0.1.0-beta.3 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +55 -63
  3. package/docs/ARCHITECTURE.md +40 -0
  4. package/docs/CLI.md +26 -2
  5. package/docs/DOCUMENTATION_SYNC.md +144 -0
  6. package/docs/INTEGRATIONS.md +2 -2
  7. package/docs/LIMITS.md +49 -0
  8. package/docs/MEMORY.md +38 -0
  9. package/docs/RELEASING.md +7 -0
  10. package/examples/codex.toml +1 -1
  11. package/examples/mcp.json +1 -1
  12. package/mcp.json +1 -1
  13. package/package.json +1 -1
  14. package/plugin.json +1 -1
  15. package/skills/geniustasker/SKILL.md +3 -0
  16. package/skills/geniustasker/references/project-reads.md +43 -0
  17. package/skills/geniustasker-documentation/SKILL.md +39 -0
  18. package/skills/geniustasker-documentation/agents/openai.yaml +4 -0
  19. package/skills/geniustasker-documentation/references/structure.md +29 -0
  20. package/skills/geniustasker-graph/SKILL.md +31 -0
  21. package/skills/geniustasker-graph/agents/openai.yaml +4 -0
  22. package/skills/geniustasker-media/SKILL.md +33 -0
  23. package/skills/geniustasker-media/agents/openai.yaml +4 -0
  24. package/skills/geniustasker-memory/SKILL.md +75 -7
  25. package/skills/geniustasker-memory/references/knowledge-workflows.md +57 -0
  26. package/src/cli.mjs +151 -0
  27. package/src/client.mjs +54 -2
  28. package/src/command-catalog.mjs +109 -5
  29. package/src/commands.mjs +15 -2
  30. package/src/documentation-graph.mjs +251 -0
  31. package/src/documentation-store.mjs +589 -0
  32. package/src/documentation-sync.mjs +517 -0
  33. package/src/documentation.mjs +136 -0
  34. package/src/knowledge.mjs +109 -0
  35. package/src/mcp.mjs +122 -3
  36. package/src/media.mjs +307 -0
  37. package/src/read-cache.mjs +111 -0
  38. package/src/transport.mjs +33 -10
@@ -1,6 +1,6 @@
1
1
  // Generated from contracts/agents.tsp. Do not edit by hand.
2
2
  export default {
3
- "maxCommandBytes": 32768,
3
+ "maxCommandBytes": 131072,
4
4
  "commands": {
5
5
  "approval.cancel": {
6
6
  "fields": [
@@ -97,11 +97,13 @@ export default {
97
97
  "memory.create": {
98
98
  "create": true,
99
99
  "fields": [
100
+ "contentRoles",
100
101
  "title",
101
102
  "productKey",
102
103
  "summary",
103
104
  "instructions",
104
- "color"
105
+ "color",
106
+ "kind"
105
107
  ],
106
108
  "module": "Memory/$",
107
109
  "permission": "memory:write"
@@ -112,15 +114,104 @@ export default {
112
114
  "module": "Memory/$",
113
115
  "permission": "memory:write"
114
116
  },
117
+ "memory.edge.create": {
118
+ "create": true,
119
+ "fields": [
120
+ "source",
121
+ "target",
122
+ "relation",
123
+ "label",
124
+ "note",
125
+ "certainty",
126
+ "references",
127
+ "provenance"
128
+ ],
129
+ "module": "Memory/MemoryEdge",
130
+ "permission": "memory:write"
131
+ },
132
+ "memory.edge.delete": {
133
+ "delete": true,
134
+ "fields": [],
135
+ "module": "Memory/MemoryEdge",
136
+ "permission": "memory:write"
137
+ },
138
+ "memory.edge.update": {
139
+ "fields": [
140
+ "source",
141
+ "target",
142
+ "relation",
143
+ "label",
144
+ "note",
145
+ "certainty",
146
+ "references",
147
+ "provenance"
148
+ ],
149
+ "module": "Memory/MemoryEdge",
150
+ "permission": "memory:write"
151
+ },
152
+ "memory.node.create": {
153
+ "create": true,
154
+ "fields": [
155
+ "imageAssetId",
156
+ "detailLevel",
157
+ "title",
158
+ "summary",
159
+ "text",
160
+ "kind",
161
+ "authority",
162
+ "certainty",
163
+ "tags",
164
+ "references",
165
+ "sourceId",
166
+ "provenance"
167
+ ],
168
+ "module": "Memory/MemoryNode",
169
+ "permission": "memory:write"
170
+ },
171
+ "memory.node.delete": {
172
+ "delete": true,
173
+ "fields": [],
174
+ "module": "Memory/MemoryNode",
175
+ "permission": "memory:write"
176
+ },
177
+ "memory.node.update": {
178
+ "fields": [
179
+ "imageAssetId",
180
+ "detailLevel",
181
+ "title",
182
+ "summary",
183
+ "text",
184
+ "kind",
185
+ "authority",
186
+ "certainty",
187
+ "tags",
188
+ "references",
189
+ "sourceId",
190
+ "provenance"
191
+ ],
192
+ "module": "Memory/MemoryNode",
193
+ "permission": "memory:write"
194
+ },
115
195
  "memory.page.create": {
116
196
  "create": true,
117
197
  "fields": [
198
+ "visibility",
199
+ "readRoles",
118
200
  "title",
119
201
  "path",
120
202
  "summary",
121
203
  "blocks",
122
204
  "tags",
123
- "provenance"
205
+ "provenance",
206
+ "sourceId",
207
+ "parentId",
208
+ "directory",
209
+ "order",
210
+ "kind",
211
+ "state",
212
+ "authority",
213
+ "section",
214
+ "metadataJson"
124
215
  ],
125
216
  "module": "Memory/MemoryPage",
126
217
  "permission": "memory:write"
@@ -133,23 +224,36 @@ export default {
133
224
  },
134
225
  "memory.page.update": {
135
226
  "fields": [
227
+ "visibility",
228
+ "readRoles",
136
229
  "title",
137
230
  "path",
138
231
  "summary",
139
232
  "blocks",
140
233
  "tags",
141
- "provenance"
234
+ "provenance",
235
+ "sourceId",
236
+ "parentId",
237
+ "directory",
238
+ "order",
239
+ "kind",
240
+ "state",
241
+ "authority",
242
+ "section",
243
+ "metadataJson"
142
244
  ],
143
245
  "module": "Memory/MemoryPage",
144
246
  "permission": "memory:write"
145
247
  },
146
248
  "memory.update": {
147
249
  "fields": [
250
+ "contentRoles",
148
251
  "title",
149
252
  "productKey",
150
253
  "summary",
151
254
  "instructions",
152
- "color"
255
+ "color",
256
+ "kind"
153
257
  ],
154
258
  "module": "Memory/$",
155
259
  "permission": "memory:write"
package/src/commands.mjs CHANGED
@@ -77,6 +77,16 @@ export function validateCommand(value) {
77
77
  ) {
78
78
  fail('invalid_arguments', 'The command must target the matching project or memory scope.');
79
79
  }
80
+ if (
81
+ command.kind === 'memory.create' &&
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 Memory with a UUID v4 as id, the same initial epoch, and scope memory:<id>. Keep source identifiers in page sourceId.',
88
+ );
89
+ }
80
90
  if (command.kind === 'ticket.plan' && command.id !== command.scope.slice(8)) {
81
91
  fail(
82
92
  'invalid_arguments',
@@ -104,12 +114,15 @@ export async function readCommandFile(path) {
104
114
  handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
105
115
  const stat = await handle.stat();
106
116
  if (!stat.isFile() || stat.size > catalog.maxCommandBytes) {
107
- fail('invalid_arguments', 'Use a regular JSON file no larger than 32 KiB.');
117
+ fail(
118
+ 'invalid_arguments',
119
+ `Use a regular JSON file no larger than ${catalog.maxCommandBytes} bytes.`,
120
+ );
108
121
  }
109
122
  const bytes = Buffer.alloc(catalog.maxCommandBytes + 1),
110
123
  result = await handle.read(bytes, 0, bytes.length, 0);
111
124
  if (result.bytesRead > catalog.maxCommandBytes) {
112
- fail('invalid_arguments', 'Command file exceeds 32 KiB.');
125
+ fail('invalid_arguments', `Command file exceeds ${catalog.maxCommandBytes} bytes.`);
113
126
  }
114
127
  return JSON.parse(bytes.subarray(0, result.bytesRead).toString('utf8'));
115
128
  } catch (error) {
@@ -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
+ }