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.
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ let currentAgentRuntime = null;
2
+ export function setAgentRuntime(runtime) {
3
+ currentAgentRuntime = runtime;
4
+ }
5
+ export function getAgentRuntime() {
6
+ return currentAgentRuntime;
7
+ }