taskify-nostr 0.1.0
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/README.md +271 -0
- package/dist/aiClient.js +40 -0
- package/dist/completions.js +637 -0
- package/dist/config.js +39 -0
- package/dist/index.js +2074 -0
- package/dist/nostrRuntime.js +888 -0
- package/dist/onboarding.js +93 -0
- package/dist/render.js +207 -0
- package/dist/shared/agentDispatcher.js +595 -0
- package/dist/shared/agentIdempotency.js +50 -0
- package/dist/shared/agentRuntime.js +7 -0
- package/dist/shared/agentSecurity.js +161 -0
- package/dist/shared/boardUtils.js +441 -0
- package/dist/shared/dateUtils.js +123 -0
- package/dist/shared/nostr.js +70 -0
- package/dist/shared/settingsTypes.js +23 -0
- package/dist/shared/taskTypes.js +12 -0
- package/dist/shared/taskUtils.js +261 -0
- package/dist/taskCache.js +59 -0
- package/package.json +44 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { toNpub } from "./nostr.js";
|
|
2
|
+
import { getAgentIdempotencyStore } from "./agentIdempotency.js";
|
|
3
|
+
import { addTrustedNpub, annotateTrust, clearTrustedNpubs, getEffectiveAgentSecurityMode, isLooselyValidTrustedNpub, removeTrustedNpub, summarizeTrustCounts, } from "./agentSecurity.js";
|
|
4
|
+
import { getAgentRuntime, } from "./agentRuntime.js";
|
|
5
|
+
function success(id, result, version = 1) {
|
|
6
|
+
return { v: version, id, ok: true, result, error: null };
|
|
7
|
+
}
|
|
8
|
+
function failure(id, code, message, details, version = 1) {
|
|
9
|
+
return {
|
|
10
|
+
v: version,
|
|
11
|
+
id,
|
|
12
|
+
ok: false,
|
|
13
|
+
result: null,
|
|
14
|
+
error: {
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
...(details && Object.keys(details).length ? { details } : {}),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isPlainObject(value) {
|
|
22
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function parseIsoString(value) {
|
|
25
|
+
if (typeof value !== "string" || !value.trim())
|
|
26
|
+
return undefined;
|
|
27
|
+
const time = Date.parse(value);
|
|
28
|
+
if (Number.isNaN(time))
|
|
29
|
+
return undefined;
|
|
30
|
+
return new Date(time).toISOString();
|
|
31
|
+
}
|
|
32
|
+
function toUpdatedISO(task) {
|
|
33
|
+
const rawUpdatedAt = task.updatedAt;
|
|
34
|
+
if (typeof rawUpdatedAt === "string" && !Number.isNaN(Date.parse(rawUpdatedAt))) {
|
|
35
|
+
return new Date(rawUpdatedAt).toISOString();
|
|
36
|
+
}
|
|
37
|
+
if (typeof task.completedAt === "string" && !Number.isNaN(Date.parse(task.completedAt))) {
|
|
38
|
+
return new Date(task.completedAt).toISOString();
|
|
39
|
+
}
|
|
40
|
+
if (typeof task.createdAt === "number" && Number.isFinite(task.createdAt)) {
|
|
41
|
+
return new Date(task.createdAt).toISOString();
|
|
42
|
+
}
|
|
43
|
+
if (typeof task.dueISO === "string" && !Number.isNaN(Date.parse(task.dueISO))) {
|
|
44
|
+
return new Date(task.dueISO).toISOString();
|
|
45
|
+
}
|
|
46
|
+
return new Date(0).toISOString();
|
|
47
|
+
}
|
|
48
|
+
function toNullableDueISO(task) {
|
|
49
|
+
if (task.dueDateEnabled === false)
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof task.dueISO === "string" && !Number.isNaN(Date.parse(task.dueISO))) {
|
|
52
|
+
return new Date(task.dueISO).toISOString();
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function buildTaskBaseSummary(task) {
|
|
57
|
+
return {
|
|
58
|
+
id: task.id,
|
|
59
|
+
title: task.title,
|
|
60
|
+
note: task.note ?? "",
|
|
61
|
+
boardId: task.boardId,
|
|
62
|
+
status: (task.completed ? "done" : "open"),
|
|
63
|
+
dueISO: toNullableDueISO(task),
|
|
64
|
+
priority: task.priority ?? null,
|
|
65
|
+
updatedISO: toUpdatedISO(task),
|
|
66
|
+
createdByNpub: toNpub(task.createdBy ?? null),
|
|
67
|
+
lastEditedByNpub: toNpub(task.lastEditedBy ?? null),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function encodeCursor(offset) {
|
|
71
|
+
const payload = JSON.stringify({ offset });
|
|
72
|
+
if (typeof Buffer !== "undefined") {
|
|
73
|
+
return Buffer.from(payload, "utf8").toString("base64");
|
|
74
|
+
}
|
|
75
|
+
return btoa(payload);
|
|
76
|
+
}
|
|
77
|
+
function decodeCursor(cursor) {
|
|
78
|
+
try {
|
|
79
|
+
const decoded = typeof Buffer !== "undefined"
|
|
80
|
+
? Buffer.from(cursor, "base64").toString("utf8")
|
|
81
|
+
: atob(cursor);
|
|
82
|
+
const parsed = JSON.parse(decoded);
|
|
83
|
+
if (!isPlainObject(parsed))
|
|
84
|
+
return null;
|
|
85
|
+
const offset = parsed.offset;
|
|
86
|
+
return typeof offset === "number" && Number.isInteger(offset) && offset >= 0 ? offset : null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function requireString(source, field, details, options) {
|
|
93
|
+
const value = source[field];
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
details[`params.${field}`] = "Expected string";
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (!options?.allowEmpty && !trimmed) {
|
|
100
|
+
details[`params.${field}`] = "Required";
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
function parseProtocolVersion(value) {
|
|
106
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
110
|
+
const parsed = Number(value.trim());
|
|
111
|
+
if (Number.isInteger(parsed) && parsed >= 1)
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function validateCommand(raw) {
|
|
117
|
+
if (!isPlainObject(raw)) {
|
|
118
|
+
return { ok: false, details: { root: "Expected a JSON object" }, id: null, version: 1 };
|
|
119
|
+
}
|
|
120
|
+
const id = typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : null;
|
|
121
|
+
const version = parseProtocolVersion(raw.v ?? raw.version);
|
|
122
|
+
const details = {};
|
|
123
|
+
if (version === null)
|
|
124
|
+
details.v = "Expected positive integer version";
|
|
125
|
+
if (!id)
|
|
126
|
+
details.id = "Required string";
|
|
127
|
+
if (typeof raw.op !== "string" || !raw.op.trim())
|
|
128
|
+
details.op = "Required string";
|
|
129
|
+
if (!isPlainObject(raw.params))
|
|
130
|
+
details.params = "Expected object";
|
|
131
|
+
if (Object.keys(details).length > 0) {
|
|
132
|
+
return { ok: false, details, id, version: version ?? 1 };
|
|
133
|
+
}
|
|
134
|
+
const op = String(raw.op).trim();
|
|
135
|
+
const params = raw.params;
|
|
136
|
+
switch (op) {
|
|
137
|
+
case "meta.help":
|
|
138
|
+
case "agent.security.get":
|
|
139
|
+
case "agent.trust.list":
|
|
140
|
+
case "agent.trust.clear":
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
value: { v: version, id: id, op, params: {} },
|
|
144
|
+
};
|
|
145
|
+
case "task.create": {
|
|
146
|
+
const nextDetails = {};
|
|
147
|
+
const title = requireString(params, "title", nextDetails);
|
|
148
|
+
const note = params.note === undefined
|
|
149
|
+
? ""
|
|
150
|
+
: typeof params.note === "string"
|
|
151
|
+
? params.note
|
|
152
|
+
: (nextDetails["params.note"] = "Expected string", "");
|
|
153
|
+
const boardId = params.boardId === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: typeof params.boardId === "string" && params.boardId.trim()
|
|
156
|
+
? params.boardId.trim()
|
|
157
|
+
: (nextDetails["params.boardId"] = "Expected string", undefined);
|
|
158
|
+
const dueISO = params.dueISO === undefined
|
|
159
|
+
? undefined
|
|
160
|
+
: parseIsoString(params.dueISO) ?? (nextDetails["params.dueISO"] = "Expected ISO 8601 string", undefined);
|
|
161
|
+
const priority = params.priority === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: params.priority === 1 || params.priority === 2 || params.priority === 3
|
|
164
|
+
? params.priority
|
|
165
|
+
: (nextDetails["params.priority"] = "Expected number 1-3", undefined);
|
|
166
|
+
const idempotencyKey = params.idempotencyKey === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: typeof params.idempotencyKey === "string" && params.idempotencyKey.trim()
|
|
169
|
+
? params.idempotencyKey.trim()
|
|
170
|
+
: (nextDetails["params.idempotencyKey"] = "Expected string", undefined);
|
|
171
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
172
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
value: {
|
|
177
|
+
v: version,
|
|
178
|
+
id: id,
|
|
179
|
+
op,
|
|
180
|
+
params: { title: title, note, ...(boardId ? { boardId } : {}), ...(dueISO ? { dueISO } : {}), ...(priority ? { priority } : {}), ...(idempotencyKey ? { idempotencyKey } : {}) },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case "task.update": {
|
|
185
|
+
const nextDetails = {};
|
|
186
|
+
const taskId = requireString(params, "taskId", nextDetails);
|
|
187
|
+
if (!isPlainObject(params.patch)) {
|
|
188
|
+
nextDetails["params.patch"] = "Expected object";
|
|
189
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
190
|
+
}
|
|
191
|
+
const rawPatch = params.patch;
|
|
192
|
+
const patch = {};
|
|
193
|
+
if (rawPatch.title !== undefined) {
|
|
194
|
+
if (typeof rawPatch.title === "string" && rawPatch.title.trim())
|
|
195
|
+
patch.title = rawPatch.title.trim();
|
|
196
|
+
else
|
|
197
|
+
nextDetails["params.patch.title"] = "Expected non-empty string";
|
|
198
|
+
}
|
|
199
|
+
if (rawPatch.note !== undefined) {
|
|
200
|
+
if (typeof rawPatch.note === "string")
|
|
201
|
+
patch.note = rawPatch.note;
|
|
202
|
+
else
|
|
203
|
+
nextDetails["params.patch.note"] = "Expected string";
|
|
204
|
+
}
|
|
205
|
+
if (rawPatch.dueISO !== undefined) {
|
|
206
|
+
if (rawPatch.dueISO === null)
|
|
207
|
+
patch.dueISO = null;
|
|
208
|
+
else {
|
|
209
|
+
const dueISO = parseIsoString(rawPatch.dueISO);
|
|
210
|
+
if (dueISO)
|
|
211
|
+
patch.dueISO = dueISO;
|
|
212
|
+
else
|
|
213
|
+
nextDetails["params.patch.dueISO"] = "Expected ISO 8601 string or null";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (rawPatch.priority !== undefined) {
|
|
217
|
+
if (rawPatch.priority === null)
|
|
218
|
+
patch.priority = null;
|
|
219
|
+
else if (rawPatch.priority === 1 || rawPatch.priority === 2 || rawPatch.priority === 3)
|
|
220
|
+
patch.priority = rawPatch.priority;
|
|
221
|
+
else
|
|
222
|
+
nextDetails["params.patch.priority"] = "Expected number 1-3 or null";
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(patch).length === 0) {
|
|
225
|
+
nextDetails["params.patch"] = "At least one patch field is required";
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
228
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
value: {
|
|
233
|
+
v: version,
|
|
234
|
+
id: id,
|
|
235
|
+
op,
|
|
236
|
+
params: { taskId: taskId, patch },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
case "task.setStatus": {
|
|
241
|
+
const nextDetails = {};
|
|
242
|
+
const taskId = requireString(params, "taskId", nextDetails);
|
|
243
|
+
const status = params.status === "open" || params.status === "done"
|
|
244
|
+
? params.status
|
|
245
|
+
: (nextDetails["params.status"] = 'Expected "open" or "done"', undefined);
|
|
246
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
247
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
value: {
|
|
252
|
+
v: version,
|
|
253
|
+
id: id,
|
|
254
|
+
op,
|
|
255
|
+
params: { taskId: taskId, status: status },
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
case "task.list": {
|
|
260
|
+
const nextDetails = {};
|
|
261
|
+
const boardId = params.boardId === undefined
|
|
262
|
+
? undefined
|
|
263
|
+
: typeof params.boardId === "string" && params.boardId.trim()
|
|
264
|
+
? params.boardId.trim()
|
|
265
|
+
: (nextDetails["params.boardId"] = "Expected string", undefined);
|
|
266
|
+
const status = params.status === undefined
|
|
267
|
+
? "open"
|
|
268
|
+
: params.status === "open" || params.status === "done" || params.status === "any"
|
|
269
|
+
? params.status
|
|
270
|
+
: (nextDetails["params.status"] = 'Expected "open", "done", or "any"', "open");
|
|
271
|
+
const query = params.query === undefined
|
|
272
|
+
? undefined
|
|
273
|
+
: typeof params.query === "string" && params.query.trim()
|
|
274
|
+
? params.query.trim()
|
|
275
|
+
: (nextDetails["params.query"] = "Expected non-empty string", undefined);
|
|
276
|
+
const limit = params.limit === undefined
|
|
277
|
+
? 50
|
|
278
|
+
: typeof params.limit === "number" && Number.isInteger(params.limit) && params.limit >= 1
|
|
279
|
+
? Math.min(200, params.limit)
|
|
280
|
+
: (nextDetails["params.limit"] = "Expected integer 1-200", 50);
|
|
281
|
+
const cursor = params.cursor === undefined
|
|
282
|
+
? undefined
|
|
283
|
+
: typeof params.cursor === "string" && params.cursor.trim()
|
|
284
|
+
? params.cursor.trim()
|
|
285
|
+
: (nextDetails["params.cursor"] = "Expected string", undefined);
|
|
286
|
+
if (cursor && decodeCursor(cursor) === null) {
|
|
287
|
+
nextDetails["params.cursor"] = "Invalid cursor";
|
|
288
|
+
}
|
|
289
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
290
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
value: {
|
|
295
|
+
v: version,
|
|
296
|
+
id: id,
|
|
297
|
+
op,
|
|
298
|
+
params: {
|
|
299
|
+
...(boardId ? { boardId } : {}),
|
|
300
|
+
status,
|
|
301
|
+
...(query ? { query } : {}),
|
|
302
|
+
limit,
|
|
303
|
+
...(cursor ? { cursor } : {}),
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "task.get": {
|
|
309
|
+
const nextDetails = {};
|
|
310
|
+
const taskId = requireString(params, "taskId", nextDetails);
|
|
311
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
312
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
value: { v: version, id: id, op, params: { taskId: taskId } },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
case "agent.security.set": {
|
|
320
|
+
const nextDetails = {};
|
|
321
|
+
const nextParams = {};
|
|
322
|
+
if (params.enabled !== undefined) {
|
|
323
|
+
if (typeof params.enabled === "boolean")
|
|
324
|
+
nextParams.enabled = params.enabled;
|
|
325
|
+
else
|
|
326
|
+
nextDetails["params.enabled"] = "Expected boolean";
|
|
327
|
+
}
|
|
328
|
+
if (params.mode !== undefined) {
|
|
329
|
+
if (params.mode === "off" || params.mode === "moderate" || params.mode === "strict")
|
|
330
|
+
nextParams.mode = params.mode;
|
|
331
|
+
else
|
|
332
|
+
nextDetails["params.mode"] = 'Expected "off", "moderate", or "strict"';
|
|
333
|
+
}
|
|
334
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
335
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
value: { v: version, id: id, op, params: nextParams },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
case "agent.trust.add":
|
|
343
|
+
case "agent.trust.remove": {
|
|
344
|
+
const nextDetails = {};
|
|
345
|
+
const npub = requireString(params, "npub", nextDetails);
|
|
346
|
+
if (npub && !isLooselyValidTrustedNpub(npub)) {
|
|
347
|
+
nextDetails["params.npub"] = 'Expected string starting with "npub1"';
|
|
348
|
+
}
|
|
349
|
+
if (Object.keys(nextDetails).length > 0) {
|
|
350
|
+
return { ok: false, details: nextDetails, id, version: version ?? 1 };
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
value: { v: version, id: id, op, params: { npub: npub.toLowerCase() } },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
details: { op: "Unsupported operation" },
|
|
361
|
+
id,
|
|
362
|
+
version: version ?? 1,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function buildHelpResult() {
|
|
367
|
+
return {
|
|
368
|
+
ops: [
|
|
369
|
+
{ op: "meta.help", paramsSchema: {} },
|
|
370
|
+
{ op: "task.create", paramsSchema: { title: "string (required)", note: "string (optional)", boardId: "string (optional)", dueISO: "ISO 8601 string (optional)", priority: "number 1-3 (optional)", idempotencyKey: "string (optional)" } },
|
|
371
|
+
{ op: "task.update", paramsSchema: { taskId: "string (required)", patch: { title: "string (optional)", note: "string (optional)", dueISO: "ISO 8601 string|null (optional)", priority: "1|2|3|null (optional)" } } },
|
|
372
|
+
{ op: "task.setStatus", paramsSchema: { taskId: "string (required)", status: '"open"|"done" (required)' } },
|
|
373
|
+
{ op: "task.list", paramsSchema: { boardId: "string (optional)", status: '"open"|"done"|"any" (optional, default "open")', query: "string (optional)", limit: "number 1-200 (optional, default 50)", cursor: "string (optional)" } },
|
|
374
|
+
{ op: "task.get", paramsSchema: { taskId: "string (required)" } },
|
|
375
|
+
{ op: "agent.security.get", paramsSchema: {} },
|
|
376
|
+
{ op: "agent.security.set", paramsSchema: { enabled: "boolean (optional)", mode: '"off"|"moderate"|"strict" (optional)' } },
|
|
377
|
+
{ op: "agent.trust.add", paramsSchema: { npub: 'string starting with "npub1" (required)' } },
|
|
378
|
+
{ op: "agent.trust.remove", paramsSchema: { npub: 'string starting with "npub1" (required)' } },
|
|
379
|
+
{ op: "agent.trust.list", paramsSchema: {} },
|
|
380
|
+
{ op: "agent.trust.clear", paramsSchema: {} },
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function getSecurityConfig(runtime) {
|
|
385
|
+
return await runtime.getAgentSecurityConfig();
|
|
386
|
+
}
|
|
387
|
+
function summarizeTaskWithTrust(task, securityConfig) {
|
|
388
|
+
return annotateTrust(buildTaskBaseSummary(task), securityConfig);
|
|
389
|
+
}
|
|
390
|
+
function maybeForbidStrictSingleItem(item, securityConfig, id, version) {
|
|
391
|
+
if (getEffectiveAgentSecurityMode(securityConfig) === "strict" && !item.trusted) {
|
|
392
|
+
return failure(id, "FORBIDDEN", "Item is not trusted in strict mode", undefined, version);
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
export async function dispatchAgentCommand(raw) {
|
|
397
|
+
let parsed;
|
|
398
|
+
try {
|
|
399
|
+
parsed = JSON.parse(raw);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return failure(null, "PARSE_JSON", "Invalid JSON");
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(parsed)) {
|
|
405
|
+
return failure(null, "VALIDATION", "Expected a single JSON object", { root: "Arrays are not supported" });
|
|
406
|
+
}
|
|
407
|
+
const validated = validateCommand(parsed);
|
|
408
|
+
if (!validated.ok) {
|
|
409
|
+
return failure(validated.id, "VALIDATION", "Command validation failed", validated.details, validated.version);
|
|
410
|
+
}
|
|
411
|
+
const command = validated.value;
|
|
412
|
+
const runtime = getAgentRuntime();
|
|
413
|
+
if (!runtime) {
|
|
414
|
+
return failure(command.id, "INTERNAL", "Agent runtime is not available", undefined, command.v);
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
switch (command.op) {
|
|
418
|
+
case "meta.help":
|
|
419
|
+
return success(command.id, buildHelpResult(), command.v);
|
|
420
|
+
case "task.create": {
|
|
421
|
+
const { title, note = "", dueISO, priority, idempotencyKey } = command.params;
|
|
422
|
+
const boardId = command.params.boardId ?? runtime.getDefaultBoardId() ?? "inbox";
|
|
423
|
+
const idempotencyStore = getAgentIdempotencyStore();
|
|
424
|
+
if (idempotencyKey) {
|
|
425
|
+
const existingTaskId = await idempotencyStore.get(idempotencyKey);
|
|
426
|
+
if (existingTaskId) {
|
|
427
|
+
const existingTask = await runtime.getTask(existingTaskId);
|
|
428
|
+
if (existingTask) {
|
|
429
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
430
|
+
return success(command.id, {
|
|
431
|
+
taskId: existingTask.id,
|
|
432
|
+
task: summarizeTaskWithTrust(existingTask, securityConfig),
|
|
433
|
+
}, command.v);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const createdTask = await runtime.createTask({
|
|
438
|
+
title,
|
|
439
|
+
note,
|
|
440
|
+
boardId,
|
|
441
|
+
...(dueISO ? { dueISO } : {}),
|
|
442
|
+
...(priority ? { priority } : {}),
|
|
443
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
444
|
+
});
|
|
445
|
+
if (idempotencyKey) {
|
|
446
|
+
await idempotencyStore.set(idempotencyKey, createdTask.id);
|
|
447
|
+
}
|
|
448
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
449
|
+
return success(command.id, {
|
|
450
|
+
taskId: createdTask.id,
|
|
451
|
+
task: summarizeTaskWithTrust(createdTask, securityConfig),
|
|
452
|
+
}, command.v);
|
|
453
|
+
}
|
|
454
|
+
case "task.update": {
|
|
455
|
+
const updatedTask = await runtime.updateTask(command.params.taskId, command.params.patch);
|
|
456
|
+
if (!updatedTask) {
|
|
457
|
+
return failure(command.id, "NOT_FOUND", "Task not found", undefined, command.v);
|
|
458
|
+
}
|
|
459
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
460
|
+
return success(command.id, { task: summarizeTaskWithTrust(updatedTask, securityConfig) }, command.v);
|
|
461
|
+
}
|
|
462
|
+
case "task.setStatus": {
|
|
463
|
+
const updatedTask = await runtime.setTaskStatus(command.params.taskId, command.params.status);
|
|
464
|
+
if (!updatedTask) {
|
|
465
|
+
return failure(command.id, "NOT_FOUND", "Task not found", undefined, command.v);
|
|
466
|
+
}
|
|
467
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
468
|
+
return success(command.id, { task: summarizeTaskWithTrust(updatedTask, securityConfig) }, command.v);
|
|
469
|
+
}
|
|
470
|
+
case "task.list": {
|
|
471
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
472
|
+
const tasks = await runtime.listTasks({
|
|
473
|
+
...(command.params.boardId ? { boardId: command.params.boardId } : {}),
|
|
474
|
+
status: command.params.status ?? "open",
|
|
475
|
+
});
|
|
476
|
+
const sorted = [...tasks].sort((left, right) => {
|
|
477
|
+
const leftUpdated = toUpdatedISO(left);
|
|
478
|
+
const rightUpdated = toUpdatedISO(right);
|
|
479
|
+
if (leftUpdated !== rightUpdated)
|
|
480
|
+
return rightUpdated.localeCompare(leftUpdated);
|
|
481
|
+
return left.id.localeCompare(right.id);
|
|
482
|
+
});
|
|
483
|
+
const annotatedAll = sorted.map((task) => summarizeTaskWithTrust(task, securityConfig));
|
|
484
|
+
const query = command.params.query?.toLowerCase();
|
|
485
|
+
const queryFiltered = query
|
|
486
|
+
? annotatedAll.filter((item) => {
|
|
487
|
+
const haystack = `${item.title}\n${item.note}`.toLowerCase();
|
|
488
|
+
return haystack.includes(query);
|
|
489
|
+
})
|
|
490
|
+
: annotatedAll;
|
|
491
|
+
const filtered = getEffectiveAgentSecurityMode(securityConfig) === "strict"
|
|
492
|
+
? queryFiltered.filter((item) => item.trusted)
|
|
493
|
+
: queryFiltered;
|
|
494
|
+
const offset = command.params.cursor ? decodeCursor(command.params.cursor) ?? 0 : 0;
|
|
495
|
+
const limit = command.params.limit ?? 50;
|
|
496
|
+
const items = filtered.slice(offset, offset + limit);
|
|
497
|
+
const nextOffset = offset + items.length;
|
|
498
|
+
const nextCursor = nextOffset < filtered.length ? encodeCursor(nextOffset) : null;
|
|
499
|
+
return success(command.id, {
|
|
500
|
+
items,
|
|
501
|
+
nextCursor,
|
|
502
|
+
counts: summarizeTrustCounts(filtered, items.length),
|
|
503
|
+
}, command.v);
|
|
504
|
+
}
|
|
505
|
+
case "task.get": {
|
|
506
|
+
const task = await runtime.getTask(command.params.taskId);
|
|
507
|
+
if (!task) {
|
|
508
|
+
return failure(command.id, "NOT_FOUND", "Task not found", undefined, command.v);
|
|
509
|
+
}
|
|
510
|
+
const securityConfig = await getSecurityConfig(runtime);
|
|
511
|
+
const summary = summarizeTaskWithTrust(task, securityConfig);
|
|
512
|
+
const strictError = maybeForbidStrictSingleItem(summary, securityConfig, command.id, command.v);
|
|
513
|
+
if (strictError)
|
|
514
|
+
return strictError;
|
|
515
|
+
return success(command.id, { task: summary }, command.v);
|
|
516
|
+
}
|
|
517
|
+
case "agent.security.get": {
|
|
518
|
+
const config = await getSecurityConfig(runtime);
|
|
519
|
+
return success(command.id, {
|
|
520
|
+
enabled: config.enabled,
|
|
521
|
+
mode: config.mode,
|
|
522
|
+
trustedNpubs: config.trustedNpubs,
|
|
523
|
+
updatedISO: config.updatedISO,
|
|
524
|
+
}, command.v);
|
|
525
|
+
}
|
|
526
|
+
case "agent.security.set": {
|
|
527
|
+
const current = await getSecurityConfig(runtime);
|
|
528
|
+
const next = {
|
|
529
|
+
enabled: command.params.enabled ?? current.enabled,
|
|
530
|
+
mode: command.params.mode ?? current.mode,
|
|
531
|
+
trustedNpubs: current.trustedNpubs,
|
|
532
|
+
updatedISO: new Date().toISOString(),
|
|
533
|
+
};
|
|
534
|
+
const saved = await runtime.setAgentSecurityConfig(next);
|
|
535
|
+
return success(command.id, {
|
|
536
|
+
enabled: saved.enabled,
|
|
537
|
+
mode: saved.mode,
|
|
538
|
+
trustedNpubs: saved.trustedNpubs,
|
|
539
|
+
updatedISO: saved.updatedISO,
|
|
540
|
+
}, command.v);
|
|
541
|
+
}
|
|
542
|
+
case "agent.trust.add": {
|
|
543
|
+
const current = await getSecurityConfig(runtime);
|
|
544
|
+
const saved = await runtime.setAgentSecurityConfig(addTrustedNpub(current, command.params.npub));
|
|
545
|
+
return success(command.id, {
|
|
546
|
+
enabled: saved.enabled,
|
|
547
|
+
mode: saved.mode,
|
|
548
|
+
trustedNpubs: saved.trustedNpubs,
|
|
549
|
+
updatedISO: saved.updatedISO,
|
|
550
|
+
}, command.v);
|
|
551
|
+
}
|
|
552
|
+
case "agent.trust.remove": {
|
|
553
|
+
const current = await getSecurityConfig(runtime);
|
|
554
|
+
const saved = await runtime.setAgentSecurityConfig(removeTrustedNpub(current, command.params.npub));
|
|
555
|
+
return success(command.id, {
|
|
556
|
+
enabled: saved.enabled,
|
|
557
|
+
mode: saved.mode,
|
|
558
|
+
trustedNpubs: saved.trustedNpubs,
|
|
559
|
+
updatedISO: saved.updatedISO,
|
|
560
|
+
}, command.v);
|
|
561
|
+
}
|
|
562
|
+
case "agent.trust.list": {
|
|
563
|
+
const config = await getSecurityConfig(runtime);
|
|
564
|
+
return success(command.id, { trustedNpubs: config.trustedNpubs }, command.v);
|
|
565
|
+
}
|
|
566
|
+
case "agent.trust.clear": {
|
|
567
|
+
const current = await getSecurityConfig(runtime);
|
|
568
|
+
const saved = await runtime.setAgentSecurityConfig(clearTrustedNpubs(current));
|
|
569
|
+
return success(command.id, {
|
|
570
|
+
enabled: saved.enabled,
|
|
571
|
+
mode: saved.mode,
|
|
572
|
+
trustedNpubs: saved.trustedNpubs,
|
|
573
|
+
updatedISO: saved.updatedISO,
|
|
574
|
+
}, command.v);
|
|
575
|
+
}
|
|
576
|
+
default: {
|
|
577
|
+
const _cmd = command;
|
|
578
|
+
return failure(_cmd.id, "VALIDATION", "Unsupported operation", { op: "Unsupported operation" }, _cmd.v);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
const code = error?.code;
|
|
584
|
+
const message = typeof error?.message === "string" && error.message ? error.message : "Internal error";
|
|
585
|
+
if (code === "PARSE_JSON"
|
|
586
|
+
|| code === "VALIDATION"
|
|
587
|
+
|| code === "NOT_FOUND"
|
|
588
|
+
|| code === "CONFLICT"
|
|
589
|
+
|| code === "FORBIDDEN"
|
|
590
|
+
|| code === "INTERNAL") {
|
|
591
|
+
return failure(command.id, code, message, undefined, command.v);
|
|
592
|
+
}
|
|
593
|
+
return failure(command.id, "INTERNAL", message, undefined, command.v);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// CLI-adapted version of agentIdempotency.ts (in-memory + file-based persistence)
|
|
2
|
+
export const AGENT_IDEMPOTENCY_STORAGE_KEY = "taskify.agent.idempotency.v1";
|
|
3
|
+
const MAX_AGENT_IDEMPOTENCY_ENTRIES = 100;
|
|
4
|
+
const AGENT_IDEMPOTENCY_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
5
|
+
function normalizeIdempotencyKey(value) {
|
|
6
|
+
return typeof value === "string" ? value.trim() : "";
|
|
7
|
+
}
|
|
8
|
+
// In-memory store for CLI use
|
|
9
|
+
const inMemoryEntries = new Map();
|
|
10
|
+
const inMemoryIdempotencyStore = {
|
|
11
|
+
async get(key) {
|
|
12
|
+
const normalizedKey = normalizeIdempotencyKey(key);
|
|
13
|
+
if (!normalizedKey)
|
|
14
|
+
return null;
|
|
15
|
+
const entry = inMemoryEntries.get(normalizedKey);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return null;
|
|
18
|
+
if (Date.now() - entry.createdAt >= AGENT_IDEMPOTENCY_TTL_MS) {
|
|
19
|
+
inMemoryEntries.delete(normalizedKey);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return entry.taskId;
|
|
23
|
+
},
|
|
24
|
+
async set(key, taskId) {
|
|
25
|
+
const normalizedKey = normalizeIdempotencyKey(key);
|
|
26
|
+
const normalizedTaskId = typeof taskId === "string" ? taskId.trim() : "";
|
|
27
|
+
if (!normalizedKey || !normalizedTaskId)
|
|
28
|
+
return;
|
|
29
|
+
inMemoryEntries.set(normalizedKey, {
|
|
30
|
+
key: normalizedKey,
|
|
31
|
+
taskId: normalizedTaskId,
|
|
32
|
+
createdAt: Date.now(),
|
|
33
|
+
});
|
|
34
|
+
// Trim to max entries (keep newest)
|
|
35
|
+
if (inMemoryEntries.size > MAX_AGENT_IDEMPOTENCY_ENTRIES) {
|
|
36
|
+
const sorted = Array.from(inMemoryEntries.values()).sort((a, b) => a.createdAt - b.createdAt);
|
|
37
|
+
const toRemove = sorted.slice(0, inMemoryEntries.size - MAX_AGENT_IDEMPOTENCY_ENTRIES);
|
|
38
|
+
for (const entry of toRemove) {
|
|
39
|
+
inMemoryEntries.delete(entry.key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
let currentAgentIdempotencyStore = inMemoryIdempotencyStore;
|
|
45
|
+
export function getAgentIdempotencyStore() {
|
|
46
|
+
return currentAgentIdempotencyStore;
|
|
47
|
+
}
|
|
48
|
+
export function setAgentIdempotencyStore(store) {
|
|
49
|
+
currentAgentIdempotencyStore = store ?? inMemoryIdempotencyStore;
|
|
50
|
+
}
|