@pulsarinteractive/geniustasker 0.1.0-beta.2 → 0.1.0-beta.4
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 +19 -0
- package/CONTRIBUTING.md +67 -12
- package/LICENSE +21 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +55 -59
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/docs/ARCHITECTURE.md +126 -0
- package/docs/CLI.md +30 -11
- package/docs/INTEGRATIONS.md +3 -3
- package/docs/LIMITS.md +49 -0
- package/docs/MEMORY.md +38 -0
- package/docs/OPENAI.md +10 -11
- package/docs/RELEASING.md +11 -3
- package/docs/TESTING.md +52 -0
- package/examples/codex.toml +1 -1
- package/examples/mcp.json +1 -7
- package/mcp.json +1 -7
- package/package.json +20 -9
- package/plugin.json +6 -15
- package/skills/geniustasker/SKILL.md +3 -2
- package/skills/geniustasker/agents/openai.yaml +3 -3
- package/skills/geniustasker/references/collaboration.md +0 -2
- package/skills/geniustasker/references/project-reads.md +43 -0
- package/skills/geniustasker-memory/SKILL.md +67 -6
- package/skills/geniustasker-memory/agents/openai.yaml +3 -3
- package/skills/geniustasker-memory/references/knowledge-workflows.md +57 -0
- package/src/changes.mjs +104 -28
- package/src/cli.mjs +372 -77
- package/src/client.mjs +201 -34
- package/src/command-catalog.mjs +109 -5
- package/src/commands.mjs +215 -52
- package/src/errors.mjs +55 -12
- package/src/knowledge.mjs +109 -0
- package/src/login.mjs +190 -47
- package/src/mcp.mjs +256 -43
- package/src/media.mjs +242 -0
- package/src/profiles.mjs +123 -32
- package/src/read-cache.mjs +111 -0
- package/src/store.mjs +101 -26
- package/src/transport.mjs +165 -32
package/src/client.mjs
CHANGED
|
@@ -1,34 +1,94 @@
|
|
|
1
1
|
import { fail } from './errors.mjs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { ConditionalReadCache } from './read-cache.mjs';
|
|
2
4
|
|
|
5
|
+
/** Validate a token response before it can replace an origin-bound local session. */
|
|
3
6
|
export function credentials(result, origin, now = Date.now()) {
|
|
4
|
-
const token = (value, kind) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const token = (value, kind) =>
|
|
8
|
+
typeof value === 'string' &&
|
|
9
|
+
new RegExp(`^gta1\\.[a-f0-9]{64}\\.wd${kind}_[A-Za-z0-9_-]{43}$`).test(value);
|
|
10
|
+
if (
|
|
11
|
+
!token(result?.access_token, 'a') ||
|
|
12
|
+
!token(result?.refresh_token, 'r') ||
|
|
13
|
+
result.token_type !== 'Bearer' ||
|
|
14
|
+
!Number.isSafeInteger(result.expires_in) ||
|
|
15
|
+
result.expires_in < 1 ||
|
|
16
|
+
result.expires_in > 3600 ||
|
|
17
|
+
['connection_id', 'profile_id', 'session_id'].some(
|
|
18
|
+
(key) => typeof result[key] !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(result[key]),
|
|
19
|
+
)
|
|
20
|
+
) {
|
|
21
|
+
fail('invalid_response', 'The authorization response was invalid.', {
|
|
22
|
+
remediation: 'Revoke the new session in Account → Agents and retry login.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
formatVersion: 1,
|
|
27
|
+
origin,
|
|
28
|
+
clientId: 'geniustasker-cli',
|
|
29
|
+
access_token: result.access_token,
|
|
30
|
+
refresh_token: result.refresh_token,
|
|
31
|
+
token_type: 'Bearer',
|
|
32
|
+
expires_in: result.expires_in,
|
|
33
|
+
connection_id: result.connection_id,
|
|
34
|
+
profile_id: result.profile_id,
|
|
35
|
+
session_id: result.session_id,
|
|
36
|
+
expiresAt: now + result.expires_in * 1000,
|
|
37
|
+
refreshPending: false,
|
|
38
|
+
};
|
|
13
39
|
}
|
|
14
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Shared delegated client for CLI and MCP operations.
|
|
43
|
+
* Renewal and profile selection serialize on the context lock; domain writes
|
|
44
|
+
* use CommandJournal so their identity and immutable request survive failures.
|
|
45
|
+
*/
|
|
15
46
|
export class AgentClient {
|
|
16
|
-
constructor(transport, store) {
|
|
47
|
+
constructor(transport, store) {
|
|
48
|
+
this.transport = transport;
|
|
49
|
+
this.store = store;
|
|
50
|
+
this.readCache = new ConditionalReadCache();
|
|
51
|
+
this.readRealm = null;
|
|
52
|
+
}
|
|
53
|
+
invalidateReads() {
|
|
54
|
+
this.readCache.clear();
|
|
55
|
+
}
|
|
17
56
|
async session() {
|
|
18
57
|
const session = await this.store.read();
|
|
19
|
-
if (!session)
|
|
20
|
-
|
|
21
|
-
|
|
58
|
+
if (!session) {
|
|
59
|
+
fail('not_authenticated', 'This context is not connected.', {
|
|
60
|
+
remediation: 'Run auth login with this context and environment.',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
session.formatVersion !== 1 ||
|
|
65
|
+
session.origin !== this.transport.origin ||
|
|
66
|
+
session.clientId !== 'geniustasker-cli'
|
|
67
|
+
) {
|
|
68
|
+
fail('credential_origin_mismatch', 'This context belongs to another API environment.', {
|
|
69
|
+
remediation:
|
|
70
|
+
'Select its original environment, or use a different --context for the new environment.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
22
73
|
// Validate private storage before ever sending any token to the network.
|
|
23
74
|
credentials(session, session.origin);
|
|
24
|
-
if (!Number.isSafeInteger(session.expiresAt))
|
|
75
|
+
if (!Number.isSafeInteger(session.expiresAt)) {
|
|
76
|
+
fail('invalid_credential_store', 'The session expiry is invalid.');
|
|
77
|
+
}
|
|
25
78
|
return session;
|
|
26
79
|
}
|
|
27
80
|
async authorizedSession() {
|
|
28
81
|
return this.store.locked(async () => {
|
|
29
82
|
const session = await this.session();
|
|
30
|
-
if (session.refreshPending)
|
|
31
|
-
|
|
83
|
+
if (session.refreshPending) {
|
|
84
|
+
fail('reauthentication_required', 'The previous renewal outcome is unknown.', {
|
|
85
|
+
remediation:
|
|
86
|
+
'Run auth logout, then auth login. Do not replay the previous refresh token.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (session.expiresAt > Date.now() + 30000) {
|
|
90
|
+
return session;
|
|
91
|
+
}
|
|
32
92
|
return this.renewLocked(session);
|
|
33
93
|
}, this.transport.signal);
|
|
34
94
|
}
|
|
@@ -36,60 +96,167 @@ export class AgentClient {
|
|
|
36
96
|
* access and refresh together so an in-flight old command cannot acquire the
|
|
37
97
|
* new profile's identity. It never extends the server's absolute lifetime. */
|
|
38
98
|
async renewLocked(session, selection) {
|
|
99
|
+
this.invalidateReads();
|
|
39
100
|
await this.store.write({ ...session, refreshPending: true });
|
|
40
101
|
let result;
|
|
41
102
|
try {
|
|
42
|
-
result = await this.transport.oauth('token', {
|
|
43
|
-
|
|
103
|
+
result = await this.transport.oauth('token', {
|
|
104
|
+
grant_type: 'refresh_token',
|
|
105
|
+
refresh_token: session.refresh_token,
|
|
106
|
+
...(selection
|
|
107
|
+
? {
|
|
108
|
+
profile_id: selection.profileId,
|
|
109
|
+
profile_revision: String(selection.expectedRevision),
|
|
110
|
+
source_revision: String(selection.expectedSourceRevision),
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
});
|
|
44
114
|
} catch (error) {
|
|
45
115
|
// These two server outcomes happen before token consumption/rotation.
|
|
46
116
|
// Every other refusal or lost reply retains the uncertainty marker.
|
|
47
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
selection &&
|
|
119
|
+
error?.status === 400 &&
|
|
120
|
+
['profile_selection_conflict', 'profile_selection_denied'].includes(error.code)
|
|
121
|
+
) {
|
|
48
122
|
await this.store.write(session);
|
|
49
|
-
error.remediation =
|
|
123
|
+
error.remediation =
|
|
124
|
+
'List profiles again. Only a human can approve broader access; a changed profile requires a fresh selection.';
|
|
50
125
|
}
|
|
51
126
|
throw error;
|
|
52
127
|
}
|
|
53
128
|
const next = credentials(result, this.transport.origin);
|
|
54
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
next.connection_id !== session.connection_id ||
|
|
131
|
+
next.profile_id !== (selection?.profileId ?? session.profile_id) ||
|
|
132
|
+
next.session_id !== session.session_id
|
|
133
|
+
) {
|
|
55
134
|
fail('invalid_response', 'Renewal returned an unexpected identity.');
|
|
135
|
+
}
|
|
56
136
|
await this.store.write(next);
|
|
57
137
|
return next;
|
|
58
138
|
}
|
|
59
139
|
async selectProfile(selection) {
|
|
60
140
|
return this.store.locked(async () => {
|
|
61
141
|
const session = await this.session();
|
|
62
|
-
if (session.refreshPending)
|
|
63
|
-
|
|
64
|
-
|
|
142
|
+
if (session.refreshPending) {
|
|
143
|
+
fail('reauthentication_required', 'The previous renewal outcome is unknown.', {
|
|
144
|
+
remediation:
|
|
145
|
+
'Run auth logout, then auth login; do not replay the previous refresh token.',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
selection.sourceProfileId !== session.profile_id ||
|
|
150
|
+
selection.connectionId !== session.connection_id
|
|
151
|
+
) {
|
|
152
|
+
fail('profile_selection_conflict', 'Another process changed this context.', {
|
|
153
|
+
status: 409,
|
|
154
|
+
remediation:
|
|
155
|
+
'Read profiles again. Use separate --context names for independent terminals.',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
65
158
|
const next = await this.renewLocked(session, selection);
|
|
66
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
connected: true,
|
|
161
|
+
connectionId: next.connection_id,
|
|
162
|
+
profileId: next.profile_id,
|
|
163
|
+
sessionId: next.session_id,
|
|
164
|
+
};
|
|
67
165
|
}, this.transport.signal);
|
|
68
166
|
}
|
|
69
|
-
async accessToken() {
|
|
167
|
+
async accessToken() {
|
|
168
|
+
return (await this.authorizedSession()).access_token;
|
|
169
|
+
}
|
|
70
170
|
async get(resource, query) {
|
|
71
|
-
if (
|
|
72
|
-
|
|
171
|
+
if (
|
|
172
|
+
![
|
|
173
|
+
'me',
|
|
174
|
+
'capabilities',
|
|
175
|
+
'projects',
|
|
176
|
+
'memories',
|
|
177
|
+
'resources',
|
|
178
|
+
'profiles',
|
|
179
|
+
'answers',
|
|
180
|
+
'changes',
|
|
181
|
+
'knowledge',
|
|
182
|
+
'reference',
|
|
183
|
+
].includes(resource)
|
|
184
|
+
) {
|
|
185
|
+
fail('invalid_arguments', 'Unknown delegated resource.');
|
|
186
|
+
}
|
|
187
|
+
let token;
|
|
188
|
+
try {
|
|
189
|
+
token = await this.accessToken();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.invalidateReads();
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
const realm = createHash('sha256').update(`${this.transport.origin}\n${token}`).digest('hex');
|
|
195
|
+
if (realm !== this.readRealm) {
|
|
196
|
+
this.invalidateReads();
|
|
197
|
+
this.readRealm = realm;
|
|
198
|
+
}
|
|
199
|
+
if (resource === 'resources') {
|
|
200
|
+
if (query && Object.hasOwn(query, 'ifNoneMatch')) {
|
|
201
|
+
fail('invalid_arguments', 'Resource validators are managed by the client.');
|
|
202
|
+
}
|
|
203
|
+
const normalized = Object.entries(query ?? {})
|
|
204
|
+
.filter(([, value]) => value !== undefined)
|
|
205
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
206
|
+
const key = JSON.stringify([realm, resource, normalized]);
|
|
207
|
+
return this.readCache.read(key, async (etag) => {
|
|
208
|
+
const result = await this.transport.request('/api/agents/v1/resources', {
|
|
209
|
+
token,
|
|
210
|
+
query: { ...query, ...(etag ? { ifNoneMatch: etag } : {}) },
|
|
211
|
+
});
|
|
212
|
+
if (
|
|
213
|
+
result?.scope !== query?.scope ||
|
|
214
|
+
result?.collection !== query?.collection ||
|
|
215
|
+
!Array.isArray(result?.items)
|
|
216
|
+
) {
|
|
217
|
+
fail('invalid_response', 'The resource response did not match the requested collection.');
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
73
222
|
// No blanket retries, no automatic writes. Revocation and ACL errors remain
|
|
74
223
|
// visible to the agent. A caller chooses how to recover using the envelope.
|
|
75
|
-
|
|
224
|
+
try {
|
|
225
|
+
return await this.transport.request(`/api/agents/v1/${resource}`, { token, query });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.invalidateReads();
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
76
230
|
}
|
|
77
231
|
async logout() {
|
|
232
|
+
this.invalidateReads();
|
|
78
233
|
return this.store.locked(async () => {
|
|
79
|
-
if (!await this.store.read())
|
|
234
|
+
if (!(await this.store.read())) {
|
|
235
|
+
return { connected: false, alreadyDisconnected: true };
|
|
236
|
+
}
|
|
80
237
|
const session = await this.session();
|
|
81
238
|
// Keep credentials on failure so revocation can be retried. A used refresh
|
|
82
239
|
// token can revoke a family whose rotation response was lost.
|
|
83
|
-
await this.transport.oauth('revoke', {
|
|
240
|
+
await this.transport.oauth('revoke', {
|
|
241
|
+
token: session.refresh_token,
|
|
242
|
+
token_type_hint: 'refresh_token',
|
|
243
|
+
});
|
|
84
244
|
await this.store.remove();
|
|
85
245
|
return { connected: false, revoked: true };
|
|
86
246
|
}, this.transport.signal);
|
|
87
247
|
}
|
|
88
248
|
async doctor() {
|
|
89
249
|
const session = await this.store.read();
|
|
90
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
node: process.versions.node,
|
|
252
|
+
origin: this.transport.origin,
|
|
253
|
+
connectedLocally: Boolean(session),
|
|
91
254
|
sameEnvironment: session ? session.origin === this.transport.origin : null,
|
|
92
|
-
needsLogin: session ? session.refreshPending === true : true,
|
|
93
|
-
|
|
255
|
+
needsLogin: session ? session.refreshPending === true : true,
|
|
256
|
+
storage: 'private OS-user file',
|
|
257
|
+
publication:
|
|
258
|
+
'CLI installation does not grant service access; this beta requires an admitted account.',
|
|
259
|
+
writes: 'Discover server capabilities before attempting changes.',
|
|
260
|
+
};
|
|
94
261
|
}
|
|
95
262
|
}
|
package/src/command-catalog.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated from contracts/agents.tsp. Do not edit by hand.
|
|
2
2
|
export default {
|
|
3
|
-
"maxCommandBytes":
|
|
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"
|