@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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: 'GeniusTasker Resources'
3
+ short_description: 'Compare and reuse private document media'
4
+ default_prompt: 'Use $geniustasker-media for the selected GeniusTasker knowledge workflow.'
@@ -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. Read `tasker_resources` with a discovered `memory:ID` scope and `collection: pageIndex`.
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. Removed files leave repairable block links.
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
@@ -180,6 +180,8 @@ export class AgentClient {
180
180
  'changes',
181
181
  'knowledge',
182
182
  'reference',
183
+ 'memory/tree',
184
+ 'memory/markdown',
183
185
  ].includes(resource)
184
186
  ) {
185
187
  fail('invalid_arguments', 'Unknown delegated resource.');
@@ -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
+ }