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,93 @@
1
+ #!/usr/bin/env node
2
+ import * as readline from "readline";
3
+ import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
4
+ import { loadConfig, saveConfig } from "./config.js";
5
+ function ask(rl, question) {
6
+ return new Promise((resolve) => rl.question(question, resolve));
7
+ }
8
+ export async function runOnboarding() {
9
+ console.log();
10
+ console.log("┌─────────────────────────────────────────┐");
11
+ console.log("│ Welcome to taskify-nostr! 🦉 │");
12
+ console.log("│ Nostr-powered task management CLI │");
13
+ console.log("└─────────────────────────────────────────┘");
14
+ console.log();
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ const cfg = await loadConfig();
20
+ // Step 1 — Private key
21
+ console.log("Step 1 — Private key");
22
+ const hasKey = await ask(rl, "Do you have a Nostr private key (nsec)? [Y/n] ");
23
+ if (hasKey.trim().toLowerCase() !== "n") {
24
+ // User has a key
25
+ let nsec = "";
26
+ while (true) {
27
+ nsec = (await ask(rl, "Paste your nsec: ")).trim();
28
+ if (nsec.startsWith("nsec1")) {
29
+ try {
30
+ nip19.decode(nsec);
31
+ break;
32
+ }
33
+ catch {
34
+ // invalid
35
+ }
36
+ }
37
+ console.log("Invalid nsec. Try again or press Ctrl+C to abort.");
38
+ }
39
+ cfg.nsec = nsec;
40
+ }
41
+ else {
42
+ // Generate new keypair
43
+ const sk = generateSecretKey();
44
+ const pk = getPublicKey(sk);
45
+ const nsec = nip19.nsecEncode(sk);
46
+ const npub = nip19.npubEncode(pk);
47
+ console.log();
48
+ console.log("✓ Generated new Nostr identity");
49
+ console.log(` npub: ${npub}`);
50
+ console.log(` nsec: ${nsec} ← KEEP THIS SECRET — it is your password`);
51
+ console.log();
52
+ console.log("Save this nsec somewhere safe. It cannot be recovered if lost.");
53
+ const cont = await ask(rl, "Continue? [Y/n] ");
54
+ if (cont.trim().toLowerCase() === "n") {
55
+ rl.close();
56
+ process.exit(0);
57
+ }
58
+ cfg.nsec = nsec;
59
+ }
60
+ // Step 2 — Default board
61
+ console.log();
62
+ console.log("Step 2 — Default board");
63
+ const joinBoard = await ask(rl, "Do you want to join an existing board? [y/N] ");
64
+ if (joinBoard.trim().toLowerCase() === "y") {
65
+ const boardId = (await ask(rl, "Board ID (Nostr event id): ")).trim();
66
+ if (boardId) {
67
+ cfg.defaultBoard = boardId;
68
+ }
69
+ }
70
+ // Step 3 — Relays
71
+ console.log();
72
+ console.log("Step 3 — Relays");
73
+ const configRelays = await ask(rl, "Configure relays? Default relays will be used if skipped. [y/N] ");
74
+ if (configRelays.trim().toLowerCase() === "y") {
75
+ const relays = [];
76
+ while (true) {
77
+ const relay = (await ask(rl, "Add relay URL (blank to finish): ")).trim();
78
+ if (!relay)
79
+ break;
80
+ relays.push(relay);
81
+ }
82
+ if (relays.length > 0) {
83
+ cfg.relays = relays;
84
+ }
85
+ }
86
+ rl.close();
87
+ await saveConfig(cfg);
88
+ // Step 4 — Done
89
+ console.log();
90
+ console.log("✓ Setup complete! Run `taskify boards` to see your boards.");
91
+ console.log(" Run `taskify --help` to explore all commands.");
92
+ console.log();
93
+ }
package/dist/render.js ADDED
@@ -0,0 +1,207 @@
1
+ import chalk from "chalk";
2
+ import { nip19 } from "nostr-tools";
3
+ import { TASK_PRIORITY_MARKS } from "./shared/taskTypes.js";
4
+ const WEEKDAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
5
+ function toNpubSafe(hex) {
6
+ // hex must be raw 32-byte (64-char) — encode directly, no prefix stripping
7
+ try {
8
+ return nip19.npubEncode(hex);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ function truncateNpub(raw) {
15
+ if (!raw)
16
+ return "unknown";
17
+ const npub = toNpubSafe(raw) ?? raw;
18
+ if (npub.length <= 16)
19
+ return npub;
20
+ return npub.slice(0, 12) + "...";
21
+ }
22
+ function npubToHex(npubOrHex) {
23
+ if (npubOrHex.startsWith("npub1")) {
24
+ try {
25
+ const decoded = nip19.decode(npubOrHex);
26
+ if (decoded.type === "npub")
27
+ return decoded.data;
28
+ }
29
+ catch { /* fall through */ }
30
+ }
31
+ return npubOrHex;
32
+ }
33
+ export function trustLabel(lastEditedBy, trustedNpubs) {
34
+ if (!lastEditedBy)
35
+ return chalk.yellow("? unknown");
36
+ // Normalize all trusted entries to raw hex for comparison
37
+ const trustedHexSet = new Set(trustedNpubs.map(npubToHex));
38
+ if (trustedHexSet.has(lastEditedBy))
39
+ return chalk.green("✓ trusted");
40
+ return chalk.red("✗ untrusted");
41
+ }
42
+ function truncateWithSubtasks(task, titleLen) {
43
+ const subtasks = task.subtasks;
44
+ let suffix = "";
45
+ if (Array.isArray(subtasks) && subtasks.length > 0) {
46
+ const done = subtasks.filter((s) => s.completed).length;
47
+ suffix = ` (${done}/${subtasks.length})`;
48
+ }
49
+ const available = titleLen - suffix.length;
50
+ let base = task.title;
51
+ if (base.length > available) {
52
+ base = base.slice(0, available - 1) + "…";
53
+ }
54
+ return (base + suffix).padEnd(titleLen);
55
+ }
56
+ function formatDue(dueISO) {
57
+ if (!dueISO)
58
+ return "".padEnd(12);
59
+ return dueISO.slice(0, 10).padEnd(12);
60
+ }
61
+ function formatPri(priority) {
62
+ if (!priority)
63
+ return "-".padEnd(4);
64
+ return String(priority).padEnd(4);
65
+ }
66
+ function formatRec(task) {
67
+ const rec = task.recurrence;
68
+ if (!rec || rec.type === "none")
69
+ return "".padEnd(5);
70
+ switch (rec.type) {
71
+ case "daily": return "DAY ";
72
+ case "weekly": return "WKL ";
73
+ case "every": return "EVR ";
74
+ case "monthlyDay": return "MON ";
75
+ default: return "".padEnd(5);
76
+ }
77
+ }
78
+ function formatBounty(task) {
79
+ const b = task.bounty;
80
+ if (!b || b.amount === undefined)
81
+ return "";
82
+ const amt = String(b.amount);
83
+ switch (b.state) {
84
+ case "claimed": return amt + "✓";
85
+ case "locked": return amt + "⏳";
86
+ case "revoked": return amt + "✗";
87
+ default: return amt;
88
+ }
89
+ }
90
+ function formatRecurrenceFull(task) {
91
+ const rec = task.recurrence;
92
+ if (!rec || rec.type === "none")
93
+ return "";
94
+ switch (rec.type) {
95
+ case "daily": return "daily";
96
+ case "weekly": {
97
+ const days = (rec.days ?? []).map((d) => WEEKDAY_NAMES[d] ?? d).join(" ");
98
+ return `weekly ${days}`.trim();
99
+ }
100
+ case "every": return `every ${rec.n} ${rec.unit}`;
101
+ case "monthlyDay": return `monthlyDay ${rec.day}${rec.interval ? ` (every ${rec.interval})` : ""}`;
102
+ default: return "";
103
+ }
104
+ }
105
+ function formatAssignee(task) {
106
+ const assignees = task.assignees;
107
+ if (!Array.isArray(assignees) || assignees.length === 0)
108
+ return "".padEnd(12);
109
+ const first = assignees[0];
110
+ const npub = toNpubSafe(first) ?? first;
111
+ const truncated = npub.length > 12 ? npub.slice(0, 9) + "..." : npub;
112
+ return truncated.padEnd(12);
113
+ }
114
+ const COL_HEADER = `${"ID".padEnd(8)} ${"TITLE".padEnd(40)} ${"DUE".padEnd(12)} ${"PRI".padEnd(4)} ${"REC".padEnd(5)} ${"BOUNTY".padEnd(8)} ${"ASSIGN".padEnd(12)} TRUST`;
115
+ export function renderTable(tasks, trustedNpubs, columnName) {
116
+ const byBoard = new Map();
117
+ for (const task of tasks) {
118
+ const group = byBoard.get(task.boardId) ?? [];
119
+ group.push(task);
120
+ byBoard.set(task.boardId, group);
121
+ }
122
+ for (const [boardId, boardTasks] of byBoard) {
123
+ const boardName = boardTasks[0]?.boardName;
124
+ let boardHeader = boardName
125
+ ? `Board: ${boardName} (${boardId.slice(0, 8)}...)`
126
+ : `Board: ${boardId}`;
127
+ if (columnName) {
128
+ boardHeader += ` • Column: ${columnName}`;
129
+ }
130
+ console.log("\n" + chalk.bold(boardHeader));
131
+ console.log(chalk.dim(COL_HEADER));
132
+ for (const task of boardTasks) {
133
+ const id = task.id.slice(0, 8).padEnd(8);
134
+ const title = truncateWithSubtasks(task, 40);
135
+ const due = formatDue(task.dueISO);
136
+ const pri = formatPri(task.priority);
137
+ const rec = formatRec(task);
138
+ const bounty = formatBounty(task).padEnd(8);
139
+ const assign = formatAssignee(task);
140
+ const trust = trustLabel(task.lastEditedBy, trustedNpubs);
141
+ console.log(`${id} ${title} ${due} ${pri} ${rec} ${bounty} ${assign} ${trust}`);
142
+ }
143
+ }
144
+ }
145
+ export function renderTaskCard(task, trustedNpubs, localReminders) {
146
+ const lbl = (s) => chalk.dim(s.padEnd(14));
147
+ console.log();
148
+ console.log(`${lbl("ID:")}${task.id.slice(0, 8)}`);
149
+ console.log(`${lbl("Board:")}${task.boardId}`);
150
+ console.log(`${lbl("Title:")}${task.title}`);
151
+ if (task.note) {
152
+ console.log(`${lbl("Note:")}${task.note}`);
153
+ }
154
+ if (task.dueISO) {
155
+ console.log(`${lbl("Due:")}${task.dueISO.slice(0, 10)}`);
156
+ }
157
+ if (task.priority) {
158
+ const mark = TASK_PRIORITY_MARKS[task.priority] ?? String(task.priority);
159
+ console.log(`${lbl("Priority:")}${mark} (${task.priority})`);
160
+ }
161
+ const statusStr = task.completed
162
+ ? `done (completed ${task.completedAt ? task.completedAt.slice(0, 10) : "?"})`
163
+ : "open";
164
+ console.log(`${lbl("Status:")}${task.completed ? chalk.green(statusStr) : statusStr}`);
165
+ const recStr = formatRecurrenceFull(task);
166
+ if (recStr) {
167
+ console.log(`${lbl("Recurrence:")}${recStr}`);
168
+ }
169
+ if (localReminders && localReminders.length > 0) {
170
+ console.log(`${lbl("Reminders:")}(device-local) ${localReminders.join(", ")}`);
171
+ }
172
+ if (task.subtasks && task.subtasks.length > 0) {
173
+ console.log(`${lbl("Subtasks:")}`);
174
+ task.subtasks.forEach((s, i) => {
175
+ const check = s.completed ? "x" : " ";
176
+ console.log(` [${i + 1}] [${check}] ${s.title}`);
177
+ });
178
+ }
179
+ if (task.bounty) {
180
+ const b = task.bounty;
181
+ console.log(`${lbl("Bounty:")}`);
182
+ if (b.amount !== undefined)
183
+ console.log(` ${lbl("Amount:")}${b.amount}`);
184
+ console.log(` ${lbl("State:")}${b.state}`);
185
+ if (b.lock)
186
+ console.log(` ${lbl("Lock:")}${b.lock}`);
187
+ if (b.mint) {
188
+ const mint = String(b.mint);
189
+ const mintDisplay = mint.length > 40 ? mint.slice(0, 37) + "..." : mint;
190
+ console.log(` ${lbl("Mint:")}${mintDisplay}`);
191
+ }
192
+ }
193
+ const createdByDisplay = truncateNpub(task.createdBy);
194
+ console.log(`${lbl("Created by:")}${createdByDisplay}`);
195
+ const editedByDisplay = truncateNpub(task.lastEditedBy);
196
+ const trust = trustLabel(task.lastEditedBy, trustedNpubs);
197
+ console.log(`${lbl("Edited by:")}${editedByDisplay} ${trust}`);
198
+ if (task.createdAt) {
199
+ const d = new Date(task.createdAt * 1000);
200
+ const dateStr = d.toISOString().slice(0, 16).replace("T", " ");
201
+ console.log(`${lbl("Created at:")}${dateStr}`);
202
+ }
203
+ console.log();
204
+ }
205
+ export function renderJson(data) {
206
+ console.log(JSON.stringify(data, null, 2));
207
+ }