@pulsarinteractive/geniustasker 0.1.0-beta.2 → 0.1.0-beta.3

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,52 @@
1
+ # Testing
2
+
3
+ ## Run the quality checks
4
+
5
+ ```sh
6
+ npm ci
7
+ npm run verify
8
+ ```
9
+
10
+ `verify` runs formatting, ESLint, tests, package inventory, public-file checks and
11
+ plugin/schema validation. CI runs the same command on Linux and macOS with Node
12
+ 22, followed by a dependency vulnerability audit. It has read-only repository
13
+ permissions and no live service credentials.
14
+
15
+ ## Focused checks
16
+
17
+ ```sh
18
+ npm run format:check
19
+ npm run lint
20
+ node --test test/login.test.mjs
21
+ node --test test/process.test.mjs
22
+ npm run check
23
+ npm audit
24
+ ```
25
+
26
+ `npm run format` applies formatting. The generated command catalog and vendored
27
+ schemas are excluded from formatting so their provenance remains stable.
28
+
29
+ ## What the suites cover
30
+
31
+ | Suite | Observable guarantees |
32
+ | --------------- | --------------------------------------------------------------------------------------------------------------- |
33
+ | Login | Consent URL validation, PKCE state/callback handling, denial, cancellation, device-flow deadlines and slow-down |
34
+ | Client/store | Private file checks, origin binding, serialized renewal and uncertain-token recovery |
35
+ | Commands | Immutable requests, concurrent writes, identity binding, uncertain receipts and conflicts |
36
+ | Profiles | Grant validation, narrowed selection, revision conflicts and rotation uncertainty |
37
+ | Changes | Cursor validation, bounded waits, cancellation and no blind retries after quota or permission failures |
38
+ | Process | Real stdio MCP discovery/calls and coordination between separate OS processes |
39
+ | Public boundary | Rejection of excluded files, credentials, symlinks and escaping imports |
40
+
41
+ Tests create and clean up their own temporary directories and loopback servers.
42
+ They do not require browser sign-in or real project data. Use meaningful scenario
43
+ names, assert behavior at public boundaries and include the failure that motivates
44
+ a regression test. Avoid sleeps as synchronization when an explicit signal works.
45
+
46
+ ## What local tests do not establish
47
+
48
+ They do not prove admission to the hosted beta, production availability, browser
49
+ provider compatibility on every device or acceptance by each agent host. Release
50
+ verification separately installs the published archive with a fresh cache,
51
+ compares registry integrity and exercises a deliberately authorized test account.
52
+ Record those checks without publishing account data.
@@ -1,3 +1,3 @@
1
1
  [mcp_servers.geniustasker]
2
2
  command = "npx"
3
- args = ["-y", "@pulsarinteractive/geniustasker@0.1.0-beta.2", "mcp", "--context", "work"]
3
+ args = ["-y", "@pulsarinteractive/geniustasker@0.1.0-beta.3", "mcp", "--context", "work"]
package/examples/mcp.json CHANGED
@@ -2,13 +2,7 @@
2
2
  "mcpServers": {
3
3
  "geniustasker": {
4
4
  "command": "npx",
5
- "args": [
6
- "-y",
7
- "@pulsarinteractive/geniustasker@0.1.0-beta.2",
8
- "mcp",
9
- "--context",
10
- "work"
11
- ]
5
+ "args": ["-y", "@pulsarinteractive/geniustasker@0.1.0-beta.3", "mcp", "--context", "work"]
12
6
  }
13
7
  }
14
8
  }
package/mcp.json CHANGED
@@ -4,13 +4,7 @@
4
4
  "geniustasker": {
5
5
  "type": "stdio",
6
6
  "command": "npx",
7
- "args": [
8
- "-y",
9
- "@pulsarinteractive/geniustasker@0.1.0-beta.2",
10
- "mcp",
11
- "--context",
12
- "work"
13
- ]
7
+ "args": ["-y", "@pulsarinteractive/geniustasker@0.1.0-beta.3", "mcp", "--context", "work"]
14
8
  }
15
9
  }
16
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulsarinteractive/geniustasker",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "private": false,
5
5
  "description": "GeniusTasker delegated CLI and local agent tools",
6
6
  "type": "module",
@@ -21,13 +21,20 @@
21
21
  "CONTRIBUTING.md",
22
22
  "SECURITY.md",
23
23
  "plugin.json",
24
- "mcp.json"
24
+ "mcp.json",
25
+ "LICENSE",
26
+ "THIRD_PARTY_NOTICES.md",
27
+ "LICENSES/"
25
28
  ],
26
29
  "scripts": {
27
30
  "test": "node --test test/*.test.mjs",
28
31
  "check": "node tools/check-package.mjs && node tools/check-public.mjs && node tools/check-plugin.mjs",
29
- "prepublishOnly": "npm test && npm run check",
30
- "check:public": "node tools/check-public.mjs"
32
+ "prepublishOnly": "npm run verify",
33
+ "check:public": "node tools/check-public.mjs",
34
+ "format": "prettier --write .",
35
+ "format:check": "prettier --check .",
36
+ "lint": "eslint .",
37
+ "verify": "npm run format:check && npm run lint && npm test && npm run check"
31
38
  },
32
39
  "dependencies": {
33
40
  "@modelcontextprotocol/server": "2.1.0",
@@ -35,10 +42,14 @@
35
42
  "zod": "4.6.5"
36
43
  },
37
44
  "devDependencies": {
45
+ "@eslint/js": "10.0.1",
38
46
  "@modelcontextprotocol/client": "2.1.0",
39
- "ajv": "8.20.0"
47
+ "ajv": "8.20.0",
48
+ "eslint": "10.11.0",
49
+ "globals": "17.12.0",
50
+ "prettier": "3.9.9"
40
51
  },
41
- "license": "UNLICENSED",
52
+ "license": "MIT",
42
53
  "publishConfig": {
43
54
  "access": "public",
44
55
  "tag": "beta",
@@ -46,11 +57,11 @@
46
57
  },
47
58
  "repository": {
48
59
  "type": "git",
49
- "url": "https://github.com/alexisnaud/geniustasker-agent-tools.git"
60
+ "url": "https://github.com/PulsarInteractive/geniustasker-agent-tools.git"
50
61
  },
51
- "homepage": "https://github.com/alexisnaud/geniustasker-agent-tools#readme",
62
+ "homepage": "https://github.com/PulsarInteractive/geniustasker-agent-tools#readme",
52
63
  "bugs": {
53
- "url": "https://github.com/alexisnaud/geniustasker-agent-tools/issues"
64
+ "url": "https://github.com/PulsarInteractive/geniustasker-agent-tools/issues"
54
65
  },
55
66
  "keywords": [
56
67
  "geniustasker",
package/plugin.json CHANGED
@@ -1,18 +1,14 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "geniustasker",
4
- "version": "0.1.0-beta.2",
4
+ "version": "0.1.0-beta.3",
5
5
  "description": "Track project work and independent product memories with your own agents.",
6
6
  "author": {
7
7
  "name": "Pulsar Interactive"
8
8
  },
9
- "repository": "https://github.com/alexisnaud/geniustasker-agent-tools",
10
- "license": "UNLICENSED",
11
- "keywords": [
12
- "productivity",
13
- "projects",
14
- "memory"
15
- ],
9
+ "repository": "https://github.com/PulsarInteractive/geniustasker-agent-tools",
10
+ "license": "MIT",
11
+ "keywords": ["productivity", "projects", "memory"],
16
12
  "extensions": {
17
13
  "com.openai": {
18
14
  "interface": {
@@ -20,13 +16,8 @@
20
16
  "shortDescription": "Projects, progress and memory for your agents",
21
17
  "developerName": "Pulsar Interactive",
22
18
  "category": "Productivity",
23
- "capabilities": [
24
- "Read",
25
- "Write"
26
- ],
27
- "defaultPrompt": [
28
- "Use GeniusTasker to review my project progress and next tasks."
29
- ]
19
+ "capabilities": ["Read", "Write"],
20
+ "defaultPrompt": ["Use GeniusTasker to review my project progress and next tasks."]
30
21
  }
31
22
  }
32
23
  }
@@ -99,8 +99,6 @@ to evade quotas. If the requested capability is unavailable, say what is pending
99
99
  and preserve the useful findings. Final progress should identify the actual
100
100
  project/ticket and distinguish completed work, validation and remaining work.
101
101
 
102
-
103
-
104
102
  ## Specialized workflows
105
103
 
106
104
  - To create, switch or restrict an agent identity, read [Profiles](references/profiles.md).
@@ -1,4 +1,4 @@
1
1
  interface:
2
- display_name: "GeniusTasker"
3
- short_description: "Work on shared projects with your own agents"
4
- default_prompt: "Use $geniustasker to review my project and record the next useful step."
2
+ display_name: 'GeniusTasker'
3
+ short_description: 'Work on shared projects with your own agents'
4
+ default_prompt: 'Use $geniustasker to review my project and record the next useful step.'
@@ -22,7 +22,6 @@ agent policy. Use `question.close` only for this profile's own question, with it
22
22
  current revision. If no answer arrives, record the blocker and proceed only with
23
23
  independent authorized work; do not invent approval or repeatedly poll rapidly.
24
24
 
25
-
26
25
  ## Leave a useful handoff
27
26
 
28
27
  Use the discovered `task.report` command with `tasks:write`, an open unarchived
@@ -42,7 +41,6 @@ is not an invitation to recreate its text. Completing/reopening the task clears
42
41
  its current work phase and retains the report as history. A report does not grant
43
42
  permission or count as approval. Use a question when a human answer is needed.
44
43
 
45
-
46
44
  ## Request and consume a precise human decision
47
45
 
48
46
  Use `approval.request` with `approvals:write` when a concrete action needs a
@@ -1,4 +1,4 @@
1
1
  interface:
2
- display_name: "GeniusTasker Memory"
3
- short_description: "Navigate and maintain independent product knowledge"
4
- default_prompt: "Use $geniustasker-memory to find the relevant product knowledge and update it with verified results."
2
+ display_name: 'GeniusTasker Memory'
3
+ short_description: 'Navigate and maintain independent product knowledge'
4
+ default_prompt: 'Use $geniustasker-memory to find the relevant product knowledge and update it with verified results.'
package/src/changes.mjs CHANGED
@@ -5,26 +5,73 @@ import { Transport } from './transport.mjs';
5
5
  import catalog from './command-catalog.mjs';
6
6
  import { fail } from './errors.mjs';
7
7
 
8
- export const changesSchema = z.object({ scope: z.string().max(256).regex(/^(project|memory):[^\u0000-\u001f]+$/u),
9
- collection: z.string().min(1).max(32), cursor: z.string().min(1).max(4096) }).strict();
10
- export const watchSchema = changesSchema.extend({ waitSeconds: z.number().int().min(1).max(catalog.changes.maxWatchSeconds).optional() }).strict();
11
- const revision = z.string().max(16).regex(/^(0|[1-9][0-9]*)$/).refine(value => BigInt(value) <= 9007199254740991n);
12
- const changePageSchema = z.object({ scope: z.string(), collection: z.string(), epoch: z.string().min(1), checkpoint: revision,
13
- items: z.array(z.discriminatedUnion('kind', [
14
- z.object({ kind: z.literal('upsert'), id: z.string().min(1), revision, data: z.record(z.string(), z.unknown()) }).strict(),
15
- z.object({ kind: z.literal('remove'), id: z.string().min(1), revision, data: z.null() }).strict(),
16
- ])), omittedIds: z.array(z.string().min(1)), nextCursor: z.string().min(1).max(4096), hasMore: z.boolean(),
17
- pollAfterSeconds: z.number().int().min(1).max(86400),
18
- }).strict();
8
+ export const changesSchema = z
9
+ .object({
10
+ scope: z
11
+ .string()
12
+ .max(256)
13
+ // eslint-disable-next-line no-control-regex -- Reject control characters in untrusted input.
14
+ .regex(/^(project|memory):[^\u0000-\u001f]+$/u),
15
+ collection: z.string().min(1).max(32),
16
+ cursor: z.string().min(1).max(4096),
17
+ })
18
+ .strict();
19
+ export const watchSchema = changesSchema
20
+ .extend({ waitSeconds: z.number().int().min(1).max(catalog.changes.maxWatchSeconds).optional() })
21
+ .strict();
22
+ const revision = z
23
+ .string()
24
+ .max(16)
25
+ .regex(/^(0|[1-9][0-9]*)$/)
26
+ .refine((value) => BigInt(value) <= 9007199254740991n);
27
+ const changePageSchema = z
28
+ .object({
29
+ scope: z.string(),
30
+ collection: z.string(),
31
+ epoch: z.string().min(1),
32
+ checkpoint: revision,
33
+ items: z.array(
34
+ z.discriminatedUnion('kind', [
35
+ z
36
+ .object({
37
+ kind: z.literal('upsert'),
38
+ id: z.string().min(1),
39
+ revision,
40
+ data: z.record(z.string(), z.unknown()),
41
+ })
42
+ .strict(),
43
+ z
44
+ .object({ kind: z.literal('remove'), id: z.string().min(1), revision, data: z.null() })
45
+ .strict(),
46
+ ]),
47
+ ),
48
+ omittedIds: z.array(z.string().min(1)),
49
+ nextCursor: z.string().min(1).max(4096),
50
+ hasMore: z.boolean(),
51
+ pollAfterSeconds: z.number().int().min(1).max(86400),
52
+ })
53
+ .strict();
19
54
  function query(input, schema) {
20
55
  const parsed = schema.safeParse(input);
21
- if (!parsed.success) fail('invalid_arguments', 'Use a project or memory scope, collection and unchanged change cursor; waitSeconds must be within the supported bound.');
56
+ if (!parsed.success) {
57
+ fail(
58
+ 'invalid_arguments',
59
+ 'Use a project or memory scope, collection and unchanged change cursor; waitSeconds must be within the supported bound.',
60
+ );
61
+ }
22
62
  return parsed.data;
23
63
  }
64
+ /** Validate the entire bounded page before a caller can adopt its next cursor. */
24
65
  export async function readChanges(client, input) {
25
- const requested = query(input, changesSchema), page = await client.get('changes', requested);
26
- if (!changePageSchema.safeParse(page).success || page.scope !== requested.scope || page.collection !== requested.collection)
66
+ const requested = query(input, changesSchema),
67
+ page = await client.get('changes', requested);
68
+ if (
69
+ !changePageSchema.safeParse(page).success ||
70
+ page.scope !== requested.scope ||
71
+ page.collection !== requested.collection
72
+ ) {
27
73
  fail('invalid_response', 'The change page was invalid; keep the previous cursor.');
74
+ }
28
75
  return page;
29
76
  }
30
77
  /** One bounded wait, not a daemon. Each poll spends the same account budget as
@@ -32,32 +79,61 @@ export async function readChanges(client, input) {
32
79
  * quotas, expired history and revocation never trigger a blind retry loop. */
33
80
  export async function watchChanges(client, input, requestSignal) {
34
81
  const { waitSeconds = 30, ...requested } = query(input, watchSchema);
35
- const deadline = new AbortController(), parents = [client.transport.signal, requestSignal].filter(Boolean);
82
+ const deadline = new AbortController(),
83
+ parents = [client.transport.signal, requestSignal].filter(Boolean);
36
84
  const parent = parents.length ? AbortSignal.any(parents) : undefined;
37
85
  const signal = parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal;
38
86
  const timer = setTimeout(() => deadline.abort(), waitSeconds * 1000);
39
- const bounded = new AgentClient(new Transport(client.transport.origin, {
40
- fetchImpl: client.transport.fetch, timeoutMs: client.transport.timeoutMs, signal,
41
- }), client.store);
42
- let page = null, polls = 0, cursor = requested.cursor;
87
+ const bounded = new AgentClient(
88
+ new Transport(client.transport.origin, {
89
+ fetchImpl: client.transport.fetch,
90
+ timeoutMs: client.transport.timeoutMs,
91
+ signal,
92
+ }),
93
+ client.store,
94
+ );
95
+ let page = null,
96
+ polls = 0,
97
+ cursor = requested.cursor;
43
98
  const timedOut = () => ({ reason: 'timeout', polls, page, nextCursor: cursor });
44
99
  try {
45
- if (parent?.aborted) fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
100
+ if (parent?.aborted) {
101
+ fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
102
+ }
46
103
  while (!signal.aborted) {
47
- page = await readChanges(bounded, { ...requested, cursor }); polls++;
104
+ page = await readChanges(bounded, { ...requested, cursor });
105
+ polls++;
48
106
  cursor = page.nextCursor;
49
- if (page.items.length || page.omittedIds.length || page.hasMore)
107
+ if (page.items.length || page.omittedIds.length || page.hasMore) {
50
108
  return { reason: 'changes', polls, page, nextCursor: cursor };
51
- await sleep(Math.max(catalog.changes.pollIntervalSeconds, page.pollAfterSeconds) * 1000, undefined, { signal });
109
+ }
110
+ await sleep(
111
+ Math.max(catalog.changes.pollIntervalSeconds, page.pollAfterSeconds) * 1000,
112
+ undefined,
113
+ { signal },
114
+ );
115
+ }
116
+ if (parent?.aborted) {
117
+ fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
52
118
  }
53
- if (parent?.aborted) fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
54
119
  return timedOut();
55
120
  } catch (error) {
56
- if (parent?.aborted) fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
57
- if (deadline.signal.aborted && (error?.name === 'AbortError' || error?.code === 'network_unavailable')) return timedOut();
121
+ if (parent?.aborted) {
122
+ fail('cancelled', 'The wait was cancelled.', { nextCursor: cursor });
123
+ }
124
+ if (
125
+ deadline.signal.aborted &&
126
+ (error?.name === 'AbortError' || error?.code === 'network_unavailable')
127
+ ) {
128
+ return timedOut();
129
+ }
58
130
  // Completed empty polls may advance the cursor. Preserve it on an error,
59
131
  // but do not claim that a failed final request was an empty successful read.
60
- if (error?.details) error.details.nextCursor = cursor;
132
+ if (error?.details) {
133
+ error.details.nextCursor = cursor;
134
+ }
61
135
  throw error;
62
- } finally { clearTimeout(timer); }
136
+ } finally {
137
+ clearTimeout(timer);
138
+ }
63
139
  }