taskify-nostr 0.1.0 → 0.2.1
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 +44 -0
- package/dist/config.js +96 -14
- package/dist/index.js +331 -54
- package/dist/nostrRuntime.js +17 -22
- package/dist/onboarding.js +83 -26
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -197,6 +197,50 @@ Adding tasks to a compound board directly is not allowed — use a child board i
|
|
|
197
197
|
|
|
198
198
|
---
|
|
199
199
|
|
|
200
|
+
## Multiple profiles
|
|
201
|
+
|
|
202
|
+
Each agent or user can have their own named Nostr identity:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# List profiles
|
|
206
|
+
taskify profile list
|
|
207
|
+
|
|
208
|
+
# Add a new identity
|
|
209
|
+
taskify profile add ink
|
|
210
|
+
|
|
211
|
+
# Switch active profile
|
|
212
|
+
taskify profile use ink
|
|
213
|
+
|
|
214
|
+
# Use a profile for one command without switching
|
|
215
|
+
taskify list --profile cody --board "Dev"
|
|
216
|
+
|
|
217
|
+
# Show profile details
|
|
218
|
+
taskify profile show
|
|
219
|
+
|
|
220
|
+
# Rename a profile
|
|
221
|
+
taskify profile rename ink writer
|
|
222
|
+
|
|
223
|
+
# Remove a profile (cannot remove the active one)
|
|
224
|
+
taskify profile remove old-profile --force
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Profiles store separate nsec, relays, boards, and trusted npubs.
|
|
228
|
+
The active profile is used by default for all commands.
|
|
229
|
+
Use `--profile <name>` (`-P <name>`) on any command to use a different profile without switching.
|
|
230
|
+
|
|
231
|
+
### Profile commands
|
|
232
|
+
|
|
233
|
+
| Command | Options | Description |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `profile list` | | List all profiles (► marks active) |
|
|
236
|
+
| `profile add <name>` | | Add a new profile (mini onboarding) |
|
|
237
|
+
| `profile use <name>` | | Switch active profile |
|
|
238
|
+
| `profile show [name]` | | Show profile details (defaults to active) |
|
|
239
|
+
| `profile remove <name>` | `--force` | Remove a profile |
|
|
240
|
+
| `profile rename <old> <new>` | | Rename a profile |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
200
244
|
## Example output
|
|
201
245
|
|
|
202
246
|
### `taskify list`
|
package/dist/config.js
CHANGED
|
@@ -4,13 +4,14 @@ import { join } from "path";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
export const CONFIG_DIR = join(homedir(), ".taskify-cli");
|
|
6
6
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
export const DEFAULT_RELAYS = [
|
|
8
|
+
"wss://relay.damus.io",
|
|
9
|
+
"wss://nos.lol",
|
|
10
|
+
"wss://relay.snort.social",
|
|
11
|
+
"wss://relay.primal.net",
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_PROFILE = {
|
|
14
|
+
relays: [...DEFAULT_RELAYS],
|
|
14
15
|
defaultBoard: "Personal",
|
|
15
16
|
trustedNpubs: [],
|
|
16
17
|
securityMode: "moderate",
|
|
@@ -18,22 +19,103 @@ const DEFAULT_CONFIG = {
|
|
|
18
19
|
boards: [],
|
|
19
20
|
taskReminders: {},
|
|
20
21
|
};
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
function profileDefaults(partial) {
|
|
23
|
+
return {
|
|
24
|
+
...DEFAULT_PROFILE,
|
|
25
|
+
...partial,
|
|
26
|
+
relays: partial.relays && partial.relays.length > 0 ? partial.relays : [...DEFAULT_RELAYS],
|
|
27
|
+
taskReminders: partial.taskReminders ?? {},
|
|
28
|
+
trustedNpubs: partial.trustedNpubs ?? [],
|
|
29
|
+
boards: partial.boards ?? [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function loadConfig(profileName) {
|
|
33
|
+
let stored;
|
|
23
34
|
try {
|
|
24
35
|
const raw = await readFile(CONFIG_PATH, "utf-8");
|
|
25
|
-
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
// Migration: detect old flat format (has nsec or relays at top level, no profiles key)
|
|
38
|
+
if (!parsed.profiles && (parsed.nsec !== undefined || Array.isArray(parsed.relays))) {
|
|
39
|
+
const profile = profileDefaults(parsed);
|
|
40
|
+
stored = {
|
|
41
|
+
activeProfile: "default",
|
|
42
|
+
profiles: { default: profile },
|
|
43
|
+
};
|
|
44
|
+
// Save migrated config
|
|
45
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
await writeFile(CONFIG_PATH, JSON.stringify(stored, null, 2), "utf-8");
|
|
47
|
+
process.stderr.write("✓ Config migrated to multi-profile format\n");
|
|
48
|
+
}
|
|
49
|
+
else if (parsed.profiles && parsed.activeProfile) {
|
|
50
|
+
stored = parsed;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// New empty or unrecognized config
|
|
54
|
+
stored = {
|
|
55
|
+
activeProfile: "default",
|
|
56
|
+
profiles: { default: { ...DEFAULT_PROFILE } },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
26
59
|
}
|
|
27
60
|
catch {
|
|
28
|
-
|
|
61
|
+
stored = {
|
|
62
|
+
activeProfile: "default",
|
|
63
|
+
profiles: { default: { ...DEFAULT_PROFILE } },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Determine which profile to use
|
|
67
|
+
const resolvedProfileName = profileName ?? stored.activeProfile;
|
|
68
|
+
const profile = stored.profiles[resolvedProfileName];
|
|
69
|
+
if (!profile) {
|
|
70
|
+
throw new Error(`Profile not found: "${resolvedProfileName}". Available: ${Object.keys(stored.profiles).join(", ")}`);
|
|
29
71
|
}
|
|
72
|
+
const merged = profileDefaults(profile);
|
|
73
|
+
// TASKIFY_NSEC env var overrides nsec for any profile
|
|
30
74
|
if (process.env.TASKIFY_NSEC) {
|
|
31
|
-
|
|
75
|
+
merged.nsec = process.env.TASKIFY_NSEC;
|
|
32
76
|
process.stderr.write("\x1b[2m(using TASKIFY_NSEC from env)\x1b[0m\n");
|
|
33
77
|
}
|
|
34
|
-
return
|
|
78
|
+
return {
|
|
79
|
+
...merged,
|
|
80
|
+
activeProfile: stored.activeProfile,
|
|
81
|
+
profiles: stored.profiles,
|
|
82
|
+
};
|
|
35
83
|
}
|
|
84
|
+
// Updates the active profile from flat cfg fields, then saves
|
|
36
85
|
export async function saveConfig(cfg) {
|
|
37
86
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
|
-
|
|
87
|
+
const profileData = {
|
|
88
|
+
nsec: cfg.nsec,
|
|
89
|
+
relays: cfg.relays,
|
|
90
|
+
defaultBoard: cfg.defaultBoard,
|
|
91
|
+
trustedNpubs: cfg.trustedNpubs,
|
|
92
|
+
securityMode: cfg.securityMode,
|
|
93
|
+
securityEnabled: cfg.securityEnabled,
|
|
94
|
+
boards: cfg.boards,
|
|
95
|
+
taskReminders: cfg.taskReminders,
|
|
96
|
+
agent: cfg.agent,
|
|
97
|
+
};
|
|
98
|
+
const stored = {
|
|
99
|
+
activeProfile: cfg.activeProfile,
|
|
100
|
+
profiles: {
|
|
101
|
+
...cfg.profiles,
|
|
102
|
+
[cfg.activeProfile]: profileData,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
await writeFile(CONFIG_PATH, JSON.stringify(stored, null, 2), "utf-8");
|
|
106
|
+
}
|
|
107
|
+
// Save the raw profiles structure (for profile management commands — does NOT rewrite active profile from flat fields)
|
|
108
|
+
export async function saveProfiles(activeProfile, profiles) {
|
|
109
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
110
|
+
await writeFile(CONFIG_PATH, JSON.stringify({ activeProfile, profiles }, null, 2), "utf-8");
|
|
111
|
+
}
|
|
112
|
+
export function getActiveProfile(cfg) {
|
|
113
|
+
return cfg.profiles[cfg.activeProfile] ?? { ...DEFAULT_PROFILE };
|
|
114
|
+
}
|
|
115
|
+
export async function setActiveProfile(cfg, name) {
|
|
116
|
+
await saveProfiles(name, cfg.profiles);
|
|
117
|
+
}
|
|
118
|
+
export function resolveProfile(cfg, name) {
|
|
119
|
+
const profileName = name ?? cfg.activeProfile;
|
|
120
|
+
return cfg.profiles[profileName] ?? { ...DEFAULT_PROFILE };
|
|
39
121
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,18 +3,22 @@ import { Command } from "commander";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { readFile, writeFile } from "fs/promises";
|
|
5
5
|
import { createInterface } from "readline";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { nip19, getPublicKey, generateSecretKey } from "nostr-tools";
|
|
8
|
+
import { loadConfig, saveConfig, saveProfiles, DEFAULT_RELAYS } from "./config.js";
|
|
8
9
|
import { createNostrRuntime } from "./nostrRuntime.js";
|
|
9
10
|
import { renderTable, renderTaskCard, renderJson } from "./render.js";
|
|
10
11
|
import { zshCompletion, bashCompletion, fishCompletion } from "./completions.js";
|
|
11
12
|
import { readCache, clearCache, CACHE_TTL_MS } from "./taskCache.js";
|
|
12
13
|
import { runOnboarding } from "./onboarding.js";
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { version } = require("../package.json");
|
|
13
16
|
const program = new Command();
|
|
14
17
|
program
|
|
15
18
|
.name("taskify")
|
|
16
|
-
.version(
|
|
17
|
-
.description("Taskify CLI — manage tasks over Nostr")
|
|
19
|
+
.version(version)
|
|
20
|
+
.description("Taskify CLI — manage tasks over Nostr")
|
|
21
|
+
.option("-P, --profile <name>", "Use a specific profile for this command (does not change active profile)");
|
|
18
22
|
// ---- Validation helpers ----
|
|
19
23
|
function validateDue(due) {
|
|
20
24
|
if (!due)
|
|
@@ -87,7 +91,7 @@ boardCmd
|
|
|
87
91
|
.command("list")
|
|
88
92
|
.description("List all configured boards")
|
|
89
93
|
.action(async () => {
|
|
90
|
-
const config = await loadConfig();
|
|
94
|
+
const config = await loadConfig(program.opts().profile);
|
|
91
95
|
if (config.boards.length === 0) {
|
|
92
96
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
93
97
|
}
|
|
@@ -109,7 +113,7 @@ boardCmd
|
|
|
109
113
|
if (!UUID_RE.test(boardId)) {
|
|
110
114
|
console.warn(chalk.yellow(`Warning: "${boardId}" does not look like a UUID.`));
|
|
111
115
|
}
|
|
112
|
-
const config = await loadConfig();
|
|
116
|
+
const config = await loadConfig(program.opts().profile);
|
|
113
117
|
const existing = config.boards.find((b) => b.id === boardId);
|
|
114
118
|
if (existing) {
|
|
115
119
|
console.log(chalk.dim(`Already on board ${existing.name} (${boardId})`));
|
|
@@ -140,7 +144,7 @@ boardCmd
|
|
|
140
144
|
.command("sync [boardId]")
|
|
141
145
|
.description("Sync board metadata (kind, columns) from Nostr")
|
|
142
146
|
.action(async (boardId) => {
|
|
143
|
-
const config = await loadConfig();
|
|
147
|
+
const config = await loadConfig(program.opts().profile);
|
|
144
148
|
if (config.boards.length === 0) {
|
|
145
149
|
console.error(chalk.red("No boards configured."));
|
|
146
150
|
process.exit(1);
|
|
@@ -164,7 +168,7 @@ boardCmd
|
|
|
164
168
|
const meta = await runtime.syncBoard(entry.id);
|
|
165
169
|
const colCount = meta.columns?.length ?? 0;
|
|
166
170
|
const kindStr = meta.kind ?? "unknown";
|
|
167
|
-
const reloadedEntry = (await loadConfig()).boards.find((b) => b.id === entry.id);
|
|
171
|
+
const reloadedEntry = (await loadConfig(program.opts().profile)).boards.find((b) => b.id === entry.id);
|
|
168
172
|
const childrenCount = reloadedEntry?.children?.length ?? 0;
|
|
169
173
|
const childrenStr = kindStr === "compound" ? `, children: ${childrenCount}` : "";
|
|
170
174
|
console.log(chalk.green(`✓ Synced: ${entry.name} (kind: ${kindStr}, columns: ${colCount}${childrenStr})`));
|
|
@@ -184,7 +188,7 @@ boardCmd
|
|
|
184
188
|
.command("leave <boardId>")
|
|
185
189
|
.description("Remove a board from config")
|
|
186
190
|
.action(async (boardId) => {
|
|
187
|
-
const config = await loadConfig();
|
|
191
|
+
const config = await loadConfig(program.opts().profile);
|
|
188
192
|
const before = config.boards.length;
|
|
189
193
|
config.boards = config.boards.filter((b) => b.id !== boardId);
|
|
190
194
|
if (config.boards.length === before) {
|
|
@@ -199,7 +203,7 @@ boardCmd
|
|
|
199
203
|
.command("columns")
|
|
200
204
|
.description("Show cached columns for all configured boards")
|
|
201
205
|
.action(async () => {
|
|
202
|
-
const config = await loadConfig();
|
|
206
|
+
const config = await loadConfig(program.opts().profile);
|
|
203
207
|
if (config.boards.length === 0) {
|
|
204
208
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
205
209
|
process.exit(0);
|
|
@@ -222,7 +226,7 @@ boardCmd
|
|
|
222
226
|
.command("children <board>")
|
|
223
227
|
.description("List children of a compound board")
|
|
224
228
|
.action(async (boardArg) => {
|
|
225
|
-
const config = await loadConfig();
|
|
229
|
+
const config = await loadConfig(program.opts().profile);
|
|
226
230
|
const entry = config.boards.find((b) => b.id === boardArg) ??
|
|
227
231
|
config.boards.find((b) => b.name.toLowerCase() === boardArg.toLowerCase());
|
|
228
232
|
if (!entry) {
|
|
@@ -254,7 +258,7 @@ program
|
|
|
254
258
|
.command("boards")
|
|
255
259
|
.description("List configured boards (alias for: board list)")
|
|
256
260
|
.action(async () => {
|
|
257
|
-
const config = await loadConfig();
|
|
261
|
+
const config = await loadConfig(program.opts().profile);
|
|
258
262
|
if (config.boards.length === 0) {
|
|
259
263
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
260
264
|
}
|
|
@@ -318,7 +322,7 @@ program
|
|
|
318
322
|
.option("--no-cache", "Do not fall back to stale cache if relay returns empty")
|
|
319
323
|
.option("--json", "Output as JSON")
|
|
320
324
|
.action(async (opts) => {
|
|
321
|
-
const config = await loadConfig();
|
|
325
|
+
const config = await loadConfig(program.opts().profile);
|
|
322
326
|
const runtime = initRuntime(config);
|
|
323
327
|
let exitCode = 0;
|
|
324
328
|
try {
|
|
@@ -380,7 +384,7 @@ program
|
|
|
380
384
|
.option("--json", "Output raw task fields as JSON")
|
|
381
385
|
.action(async (taskId, opts) => {
|
|
382
386
|
warnShortTaskId(taskId);
|
|
383
|
-
const config = await loadConfig();
|
|
387
|
+
const config = await loadConfig(program.opts().profile);
|
|
384
388
|
const runtime = initRuntime(config);
|
|
385
389
|
let exitCode = 0;
|
|
386
390
|
try {
|
|
@@ -413,7 +417,7 @@ program
|
|
|
413
417
|
.option("--board <id|name>", "Limit to a specific board")
|
|
414
418
|
.option("--json", "Output as JSON")
|
|
415
419
|
.action(async (query, opts) => {
|
|
416
|
-
const config = await loadConfig();
|
|
420
|
+
const config = await loadConfig(program.opts().profile);
|
|
417
421
|
const runtime = initRuntime(config);
|
|
418
422
|
let exitCode = 0;
|
|
419
423
|
try {
|
|
@@ -460,7 +464,7 @@ program
|
|
|
460
464
|
console.error(chalk.red(`Invalid reminder preset(s): ${invalid.join(", ")}. Valid: ${[...VALID_REMINDER_PRESETS].join(", ")}`));
|
|
461
465
|
process.exit(1);
|
|
462
466
|
}
|
|
463
|
-
const config = await loadConfig();
|
|
467
|
+
const config = await loadConfig(program.opts().profile);
|
|
464
468
|
const runtime = initRuntime(config);
|
|
465
469
|
let exitCode = 0;
|
|
466
470
|
try {
|
|
@@ -502,7 +506,7 @@ program
|
|
|
502
506
|
.action(async (title, opts) => {
|
|
503
507
|
validateDue(opts.due);
|
|
504
508
|
validatePriority(opts.priority);
|
|
505
|
-
const config = await loadConfig();
|
|
509
|
+
const config = await loadConfig(program.opts().profile);
|
|
506
510
|
const boardId = await resolveBoardId(opts.board, config);
|
|
507
511
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
508
512
|
// Block add on compound boards
|
|
@@ -576,7 +580,7 @@ program
|
|
|
576
580
|
.option("--json", "Output updated task as JSON")
|
|
577
581
|
.action(async (taskId, opts) => {
|
|
578
582
|
warnShortTaskId(taskId);
|
|
579
|
-
const config = await loadConfig();
|
|
583
|
+
const config = await loadConfig(program.opts().profile);
|
|
580
584
|
const boardId = await resolveBoardId(opts.board, config);
|
|
581
585
|
const runtime = initRuntime(config);
|
|
582
586
|
let exitCode = 0;
|
|
@@ -610,7 +614,7 @@ program
|
|
|
610
614
|
.option("--json", "Output updated task as JSON")
|
|
611
615
|
.action(async (taskId, opts) => {
|
|
612
616
|
warnShortTaskId(taskId);
|
|
613
|
-
const config = await loadConfig();
|
|
617
|
+
const config = await loadConfig(program.opts().profile);
|
|
614
618
|
const boardId = await resolveBoardId(opts.board, config);
|
|
615
619
|
const runtime = initRuntime(config);
|
|
616
620
|
let exitCode = 0;
|
|
@@ -645,7 +649,7 @@ program
|
|
|
645
649
|
.option("--json", "Output deleted task as JSON")
|
|
646
650
|
.action(async (taskId, opts) => {
|
|
647
651
|
warnShortTaskId(taskId);
|
|
648
|
-
const config = await loadConfig();
|
|
652
|
+
const config = await loadConfig(program.opts().profile);
|
|
649
653
|
const boardId = await resolveBoardId(opts.board, config);
|
|
650
654
|
const runtime = initRuntime(config);
|
|
651
655
|
let exitCode = 0;
|
|
@@ -712,7 +716,7 @@ program
|
|
|
712
716
|
process.exit(1);
|
|
713
717
|
}
|
|
714
718
|
warnShortTaskId(taskId);
|
|
715
|
-
const config = await loadConfig();
|
|
719
|
+
const config = await loadConfig(program.opts().profile);
|
|
716
720
|
const boardId = await resolveBoardId(opts.board, config);
|
|
717
721
|
const runtime = initRuntime(config);
|
|
718
722
|
let exitCode = 0;
|
|
@@ -767,7 +771,7 @@ program
|
|
|
767
771
|
warnShortTaskId(taskId);
|
|
768
772
|
validateDue(opts.due);
|
|
769
773
|
validatePriority(opts.priority);
|
|
770
|
-
const config = await loadConfig();
|
|
774
|
+
const config = await loadConfig(program.opts().profile);
|
|
771
775
|
const boardId = await resolveBoardId(opts.board, config);
|
|
772
776
|
const runtime = initRuntime(config);
|
|
773
777
|
let exitCode = 0;
|
|
@@ -820,7 +824,7 @@ trust
|
|
|
820
824
|
.command("add <npub>")
|
|
821
825
|
.description("Add a trusted npub")
|
|
822
826
|
.action(async (npub) => {
|
|
823
|
-
const config = await loadConfig();
|
|
827
|
+
const config = await loadConfig(program.opts().profile);
|
|
824
828
|
if (!config.trustedNpubs.includes(npub)) {
|
|
825
829
|
config.trustedNpubs.push(npub);
|
|
826
830
|
}
|
|
@@ -832,7 +836,7 @@ trust
|
|
|
832
836
|
.command("remove <npub>")
|
|
833
837
|
.description("Remove a trusted npub")
|
|
834
838
|
.action(async (npub) => {
|
|
835
|
-
const config = await loadConfig();
|
|
839
|
+
const config = await loadConfig(program.opts().profile);
|
|
836
840
|
config.trustedNpubs = config.trustedNpubs.filter((n) => n !== npub);
|
|
837
841
|
await saveConfig(config);
|
|
838
842
|
console.log(chalk.green("✓ Removed"));
|
|
@@ -842,7 +846,7 @@ trust
|
|
|
842
846
|
.command("list")
|
|
843
847
|
.description("List trusted npubs")
|
|
844
848
|
.action(async () => {
|
|
845
|
-
const config = await loadConfig();
|
|
849
|
+
const config = await loadConfig(program.opts().profile);
|
|
846
850
|
if (config.trustedNpubs.length === 0) {
|
|
847
851
|
console.log(chalk.dim("No trusted npubs."));
|
|
848
852
|
}
|
|
@@ -859,7 +863,7 @@ relayCmd
|
|
|
859
863
|
.command("status")
|
|
860
864
|
.description("Show connection status of relays in the NDK pool")
|
|
861
865
|
.action(async () => {
|
|
862
|
-
const config = await loadConfig();
|
|
866
|
+
const config = await loadConfig(program.opts().profile);
|
|
863
867
|
const runtime = initRuntime(config);
|
|
864
868
|
let exitCode = 0;
|
|
865
869
|
try {
|
|
@@ -891,7 +895,7 @@ relayCmd
|
|
|
891
895
|
.command("list")
|
|
892
896
|
.description("Show configured relays with live connection check")
|
|
893
897
|
.action(async () => {
|
|
894
|
-
const config = await loadConfig();
|
|
898
|
+
const config = await loadConfig(program.opts().profile);
|
|
895
899
|
if (config.relays.length === 0) {
|
|
896
900
|
console.log(chalk.dim("No relays configured."));
|
|
897
901
|
process.exit(0);
|
|
@@ -912,7 +916,7 @@ relayCmd
|
|
|
912
916
|
.command("add <url>")
|
|
913
917
|
.description("Add a relay URL to config")
|
|
914
918
|
.action(async (url) => {
|
|
915
|
-
const config = await loadConfig();
|
|
919
|
+
const config = await loadConfig(program.opts().profile);
|
|
916
920
|
if (!config.relays.includes(url)) {
|
|
917
921
|
config.relays.push(url);
|
|
918
922
|
await saveConfig(config);
|
|
@@ -927,7 +931,7 @@ relayCmd
|
|
|
927
931
|
.command("remove <url>")
|
|
928
932
|
.description("Remove a relay URL from config")
|
|
929
933
|
.action(async (url) => {
|
|
930
|
-
const config = await loadConfig();
|
|
934
|
+
const config = await loadConfig(program.opts().profile);
|
|
931
935
|
const before = config.relays.length;
|
|
932
936
|
config.relays = config.relays.filter((r) => r !== url);
|
|
933
937
|
if (config.relays.length === before) {
|
|
@@ -952,7 +956,7 @@ cacheCmd
|
|
|
952
956
|
.command("status")
|
|
953
957
|
.description("Show per-board cache age and task count")
|
|
954
958
|
.action(async () => {
|
|
955
|
-
const config = await loadConfig();
|
|
959
|
+
const config = await loadConfig(program.opts().profile);
|
|
956
960
|
const cache = readCache();
|
|
957
961
|
const now = Date.now();
|
|
958
962
|
if (Object.keys(cache.boards).length === 0) {
|
|
@@ -1000,7 +1004,7 @@ configSet
|
|
|
1000
1004
|
console.error(chalk.red(`Invalid nsec: must start with "nsec1".`));
|
|
1001
1005
|
process.exit(1);
|
|
1002
1006
|
}
|
|
1003
|
-
const config = await loadConfig();
|
|
1007
|
+
const config = await loadConfig(program.opts().profile);
|
|
1004
1008
|
config.nsec = nsec;
|
|
1005
1009
|
await saveConfig(config);
|
|
1006
1010
|
console.log(chalk.green("✓ nsec saved"));
|
|
@@ -1010,7 +1014,7 @@ configSet
|
|
|
1010
1014
|
.command("relay <url>")
|
|
1011
1015
|
.description("Add a relay URL")
|
|
1012
1016
|
.action(async (url) => {
|
|
1013
|
-
const config = await loadConfig();
|
|
1017
|
+
const config = await loadConfig(program.opts().profile);
|
|
1014
1018
|
if (!config.relays.includes(url)) {
|
|
1015
1019
|
config.relays.push(url);
|
|
1016
1020
|
}
|
|
@@ -1054,7 +1058,7 @@ configCmd
|
|
|
1054
1058
|
.command("show")
|
|
1055
1059
|
.description("Show current config")
|
|
1056
1060
|
.action(async () => {
|
|
1057
|
-
const config = await loadConfig();
|
|
1061
|
+
const config = await loadConfig(program.opts().profile);
|
|
1058
1062
|
const display = {
|
|
1059
1063
|
...config,
|
|
1060
1064
|
nsec: config.nsec ? "nsec1****" : undefined,
|
|
@@ -1122,7 +1126,7 @@ agentConfigCmd
|
|
|
1122
1126
|
.command("set-key <key>")
|
|
1123
1127
|
.description("Set the AI API key")
|
|
1124
1128
|
.action(async (key) => {
|
|
1125
|
-
const config = await loadConfig();
|
|
1129
|
+
const config = await loadConfig(program.opts().profile);
|
|
1126
1130
|
if (!config.agent)
|
|
1127
1131
|
config.agent = {};
|
|
1128
1132
|
config.agent.apiKey = key;
|
|
@@ -1134,7 +1138,7 @@ agentConfigCmd
|
|
|
1134
1138
|
.command("set-model <model>")
|
|
1135
1139
|
.description("Set the AI model")
|
|
1136
1140
|
.action(async (model) => {
|
|
1137
|
-
const config = await loadConfig();
|
|
1141
|
+
const config = await loadConfig(program.opts().profile);
|
|
1138
1142
|
if (!config.agent)
|
|
1139
1143
|
config.agent = {};
|
|
1140
1144
|
config.agent.model = model;
|
|
@@ -1146,7 +1150,7 @@ agentConfigCmd
|
|
|
1146
1150
|
.command("set-url <url>")
|
|
1147
1151
|
.description("Set the AI base URL (OpenAI-compatible)")
|
|
1148
1152
|
.action(async (url) => {
|
|
1149
|
-
const config = await loadConfig();
|
|
1153
|
+
const config = await loadConfig(program.opts().profile);
|
|
1150
1154
|
if (!config.agent)
|
|
1151
1155
|
config.agent = {};
|
|
1152
1156
|
config.agent.baseUrl = url;
|
|
@@ -1158,7 +1162,7 @@ agentConfigCmd
|
|
|
1158
1162
|
.command("show")
|
|
1159
1163
|
.description("Show current agent config (masks API key)")
|
|
1160
1164
|
.action(async () => {
|
|
1161
|
-
const config = await loadConfig();
|
|
1165
|
+
const config = await loadConfig(program.opts().profile);
|
|
1162
1166
|
const ag = config.agent ?? {};
|
|
1163
1167
|
const rawKey = ag.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1164
1168
|
let maskedKey = "(not set)";
|
|
@@ -1182,7 +1186,7 @@ agentCmd
|
|
|
1182
1186
|
.option("--dry-run", "Show extracted fields without creating")
|
|
1183
1187
|
.option("--json", "Output created task as JSON")
|
|
1184
1188
|
.action(async (description, opts) => {
|
|
1185
|
-
const config = await loadConfig();
|
|
1189
|
+
const config = await loadConfig(program.opts().profile);
|
|
1186
1190
|
const apiKey = config.agent?.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1187
1191
|
if (!apiKey) {
|
|
1188
1192
|
console.error(chalk.red("No AI API key configured. Run: taskify agent config set-key <key>"));
|
|
@@ -1311,7 +1315,7 @@ agentCmd
|
|
|
1311
1315
|
.option("--dry-run", "Show suggestions without applying")
|
|
1312
1316
|
.option("--json", "Output suggestions as JSON")
|
|
1313
1317
|
.action(async (opts) => {
|
|
1314
|
-
const config = await loadConfig();
|
|
1318
|
+
const config = await loadConfig(program.opts().profile);
|
|
1315
1319
|
const apiKey = config.agent?.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1316
1320
|
if (!apiKey) {
|
|
1317
1321
|
console.error(chalk.red("No AI API key configured. Run: taskify agent config set-key <key>"));
|
|
@@ -1491,7 +1495,7 @@ program
|
|
|
1491
1495
|
.option("--status <open|done|any>", "Status filter (default: open)", "open")
|
|
1492
1496
|
.option("--output <file>", "Write to file instead of stdout")
|
|
1493
1497
|
.action(async (opts) => {
|
|
1494
|
-
const config = await loadConfig();
|
|
1498
|
+
const config = await loadConfig(program.opts().profile);
|
|
1495
1499
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1496
1500
|
const runtime = initRuntime(config);
|
|
1497
1501
|
let exitCode = 0;
|
|
@@ -1596,7 +1600,7 @@ program
|
|
|
1596
1600
|
.option("--dry-run", "Print preview but do not create tasks")
|
|
1597
1601
|
.option("--yes", "Skip confirmation prompt")
|
|
1598
1602
|
.action(async (file, opts) => {
|
|
1599
|
-
const config = await loadConfig();
|
|
1603
|
+
const config = await loadConfig(program.opts().profile);
|
|
1600
1604
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1601
1605
|
let raw;
|
|
1602
1606
|
try {
|
|
@@ -1743,7 +1747,7 @@ inboxCmd
|
|
|
1743
1747
|
.description("List inbox tasks (inboxItem: true)")
|
|
1744
1748
|
.option("--board <id|name>", "Board to list from")
|
|
1745
1749
|
.action(async (opts) => {
|
|
1746
|
-
const config = await loadConfig();
|
|
1750
|
+
const config = await loadConfig(program.opts().profile);
|
|
1747
1751
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1748
1752
|
const runtime = initRuntime(config);
|
|
1749
1753
|
let exitCode = 0;
|
|
@@ -1771,7 +1775,7 @@ inboxCmd
|
|
|
1771
1775
|
.description("Capture a task to inbox (inboxItem: true)")
|
|
1772
1776
|
.option("--board <id|name>", "Board to add to")
|
|
1773
1777
|
.action(async (title, opts) => {
|
|
1774
|
-
const config = await loadConfig();
|
|
1778
|
+
const config = await loadConfig(program.opts().profile);
|
|
1775
1779
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1776
1780
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
1777
1781
|
if (boardEntry.kind === "compound") {
|
|
@@ -1810,7 +1814,7 @@ inboxCmd
|
|
|
1810
1814
|
validateDue(opts.due);
|
|
1811
1815
|
validatePriority(opts.priority);
|
|
1812
1816
|
warnShortTaskId(taskId);
|
|
1813
|
-
const config = await loadConfig();
|
|
1817
|
+
const config = await loadConfig(program.opts().profile);
|
|
1814
1818
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1815
1819
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
1816
1820
|
const runtime = initRuntime(config);
|
|
@@ -1925,7 +1929,7 @@ boardCmd
|
|
|
1925
1929
|
process.exit(1);
|
|
1926
1930
|
}
|
|
1927
1931
|
const kind = opts.kind;
|
|
1928
|
-
const config = await loadConfig();
|
|
1932
|
+
const config = await loadConfig(program.opts().profile);
|
|
1929
1933
|
const runtime = initRuntime(config);
|
|
1930
1934
|
let exitCode = 0;
|
|
1931
1935
|
try {
|
|
@@ -1967,7 +1971,7 @@ program
|
|
|
1967
1971
|
.action(async (taskId, npubOrHex, opts) => {
|
|
1968
1972
|
warnShortTaskId(taskId);
|
|
1969
1973
|
const hex = npubOrHexToHex(npubOrHex);
|
|
1970
|
-
const config = await loadConfig();
|
|
1974
|
+
const config = await loadConfig(program.opts().profile);
|
|
1971
1975
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1972
1976
|
const runtime = initRuntime(config);
|
|
1973
1977
|
let exitCode = 0;
|
|
@@ -2013,7 +2017,7 @@ program
|
|
|
2013
2017
|
.action(async (taskId, npubOrHex, opts) => {
|
|
2014
2018
|
warnShortTaskId(taskId);
|
|
2015
2019
|
const hex = npubOrHexToHex(npubOrHex);
|
|
2016
|
-
const config = await loadConfig();
|
|
2020
|
+
const config = await loadConfig(program.opts().profile);
|
|
2017
2021
|
const boardId = await resolveBoardId(opts.board, config);
|
|
2018
2022
|
const runtime = initRuntime(config);
|
|
2019
2023
|
let exitCode = 0;
|
|
@@ -2046,27 +2050,300 @@ program
|
|
|
2046
2050
|
process.exit(exitCode);
|
|
2047
2051
|
}
|
|
2048
2052
|
});
|
|
2053
|
+
// ---- Helper: readline queue (handles piped stdin correctly) ----
|
|
2054
|
+
function makeLineQueue(rl) {
|
|
2055
|
+
const lineQueue = [];
|
|
2056
|
+
const waiters = [];
|
|
2057
|
+
rl.on("line", (line) => {
|
|
2058
|
+
if (waiters.length > 0) {
|
|
2059
|
+
waiters.shift()(line);
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
lineQueue.push(line);
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
return (prompt) => {
|
|
2066
|
+
process.stdout.write(prompt);
|
|
2067
|
+
return new Promise((resolve) => {
|
|
2068
|
+
if (lineQueue.length > 0) {
|
|
2069
|
+
resolve(lineQueue.shift());
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
waiters.push(resolve);
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
// ---- profile command group ----
|
|
2078
|
+
const profileCmd = program
|
|
2079
|
+
.command("profile")
|
|
2080
|
+
.description("Manage named Nostr identity profiles");
|
|
2081
|
+
// Helper to get npub string from nsec
|
|
2082
|
+
function nsecToNpub(nsec) {
|
|
2083
|
+
try {
|
|
2084
|
+
const decoded = nip19.decode(nsec);
|
|
2085
|
+
if (decoded.type === "nsec") {
|
|
2086
|
+
const pk = getPublicKey(decoded.data);
|
|
2087
|
+
return nip19.npubEncode(pk);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
catch { /* ignore */ }
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
profileCmd
|
|
2094
|
+
.command("list")
|
|
2095
|
+
.description("List all profiles (► marks active)")
|
|
2096
|
+
.action(async () => {
|
|
2097
|
+
const config = await loadConfig(program.opts().profile);
|
|
2098
|
+
for (const [name, profile] of Object.entries(config.profiles)) {
|
|
2099
|
+
const isActive = name === config.activeProfile;
|
|
2100
|
+
const marker = isActive ? "►" : " ";
|
|
2101
|
+
let npubStr = "(no key)";
|
|
2102
|
+
if (profile.nsec) {
|
|
2103
|
+
const npub = nsecToNpub(profile.nsec);
|
|
2104
|
+
if (npub)
|
|
2105
|
+
npubStr = npub.slice(0, 12) + "..." + npub.slice(-4);
|
|
2106
|
+
}
|
|
2107
|
+
const boardCount = profile.boards?.length ?? 0;
|
|
2108
|
+
console.log(` ${marker} ${name.padEnd(14)} ${npubStr.padEnd(22)} ${boardCount} board${boardCount !== 1 ? "s" : ""}`);
|
|
2109
|
+
}
|
|
2110
|
+
process.exit(0);
|
|
2111
|
+
});
|
|
2112
|
+
profileCmd
|
|
2113
|
+
.command("add <name>")
|
|
2114
|
+
.description("Add a new profile (runs mini onboarding for the new identity)")
|
|
2115
|
+
.option("--nsec <key>", "Nostr private key (skips interactive prompt)")
|
|
2116
|
+
.option("--relay <url>", "Add a relay (repeatable)", (val, acc) => { acc.push(val); return acc; }, [])
|
|
2117
|
+
.action(async (name, opts) => {
|
|
2118
|
+
const config = await loadConfig(program.opts().profile);
|
|
2119
|
+
if (config.profiles[name]) {
|
|
2120
|
+
console.error(chalk.red(`Profile already exists: "${name}"`));
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
// Non-interactive mode when --nsec is provided
|
|
2124
|
+
if (opts.nsec !== undefined) {
|
|
2125
|
+
const nsecInput = opts.nsec.trim();
|
|
2126
|
+
if (!nsecInput.startsWith("nsec1")) {
|
|
2127
|
+
console.error(chalk.red("Invalid nsec key"));
|
|
2128
|
+
process.exit(1);
|
|
2129
|
+
}
|
|
2130
|
+
try {
|
|
2131
|
+
nip19.decode(nsecInput);
|
|
2132
|
+
}
|
|
2133
|
+
catch {
|
|
2134
|
+
console.error(chalk.red("Invalid nsec key"));
|
|
2135
|
+
process.exit(1);
|
|
2136
|
+
}
|
|
2137
|
+
const relays = opts.relay.length > 0 ? opts.relay : [...DEFAULT_RELAYS];
|
|
2138
|
+
const newProfile = {
|
|
2139
|
+
nsec: nsecInput,
|
|
2140
|
+
relays,
|
|
2141
|
+
boards: [],
|
|
2142
|
+
trustedNpubs: [],
|
|
2143
|
+
securityMode: "moderate",
|
|
2144
|
+
securityEnabled: true,
|
|
2145
|
+
defaultBoard: "Personal",
|
|
2146
|
+
taskReminders: {},
|
|
2147
|
+
};
|
|
2148
|
+
const newProfiles = { ...config.profiles, [name]: newProfile };
|
|
2149
|
+
await saveProfiles(config.activeProfile, newProfiles);
|
|
2150
|
+
console.log(chalk.green(`✓ Profile '${name}' created.`));
|
|
2151
|
+
process.exit(0);
|
|
2152
|
+
}
|
|
2153
|
+
// Interactive mode
|
|
2154
|
+
console.log();
|
|
2155
|
+
console.log(chalk.bold(`Setting up profile: ${name}`));
|
|
2156
|
+
console.log();
|
|
2157
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2158
|
+
const ask = makeLineQueue(rl);
|
|
2159
|
+
// Key setup
|
|
2160
|
+
const hasKey = await ask("Do you have a Nostr private key (nsec)? [Y/n] ");
|
|
2161
|
+
let nsec;
|
|
2162
|
+
if (hasKey.trim().toLowerCase() !== "n") {
|
|
2163
|
+
while (true) {
|
|
2164
|
+
const input = (await ask("Paste your nsec: ")).trim();
|
|
2165
|
+
if (input.startsWith("nsec1")) {
|
|
2166
|
+
try {
|
|
2167
|
+
nip19.decode(input);
|
|
2168
|
+
nsec = input;
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
catch { /* invalid */ }
|
|
2172
|
+
}
|
|
2173
|
+
console.log("Invalid nsec. Try again or press Ctrl+C to abort.");
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
else {
|
|
2177
|
+
const sk = generateSecretKey();
|
|
2178
|
+
const pk = getPublicKey(sk);
|
|
2179
|
+
nsec = nip19.nsecEncode(sk);
|
|
2180
|
+
const npub = nip19.npubEncode(pk);
|
|
2181
|
+
console.log();
|
|
2182
|
+
console.log("✓ Generated new Nostr identity");
|
|
2183
|
+
console.log(` npub: ${npub}`);
|
|
2184
|
+
console.log(` nsec: ${nsec} ← KEEP THIS SECRET — it is your password`);
|
|
2185
|
+
console.log();
|
|
2186
|
+
console.log("Save this nsec somewhere safe. It cannot be recovered if lost.");
|
|
2187
|
+
const cont = await ask("Continue? [Y/n] ");
|
|
2188
|
+
if (cont.trim().toLowerCase() === "n") {
|
|
2189
|
+
rl.close();
|
|
2190
|
+
process.exit(0);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
// Relays setup
|
|
2194
|
+
console.log();
|
|
2195
|
+
let relays = [...DEFAULT_RELAYS];
|
|
2196
|
+
const useDefaults = await ask("Use default relays? [Y/n] ");
|
|
2197
|
+
if (useDefaults.trim().toLowerCase() === "n") {
|
|
2198
|
+
relays = [];
|
|
2199
|
+
while (true) {
|
|
2200
|
+
const relay = (await ask("Add relay URL (blank to finish): ")).trim();
|
|
2201
|
+
if (!relay)
|
|
2202
|
+
break;
|
|
2203
|
+
relays.push(relay);
|
|
2204
|
+
}
|
|
2205
|
+
if (relays.length === 0)
|
|
2206
|
+
relays = [...DEFAULT_RELAYS];
|
|
2207
|
+
}
|
|
2208
|
+
rl.close();
|
|
2209
|
+
const newProfile = {
|
|
2210
|
+
nsec,
|
|
2211
|
+
relays,
|
|
2212
|
+
boards: [],
|
|
2213
|
+
trustedNpubs: [],
|
|
2214
|
+
securityMode: "moderate",
|
|
2215
|
+
securityEnabled: true,
|
|
2216
|
+
defaultBoard: "Personal",
|
|
2217
|
+
taskReminders: {},
|
|
2218
|
+
};
|
|
2219
|
+
const newProfiles = { ...config.profiles, [name]: newProfile };
|
|
2220
|
+
await saveProfiles(config.activeProfile, newProfiles);
|
|
2221
|
+
console.log();
|
|
2222
|
+
console.log(chalk.green(`✓ Profile '${name}' created. Run: taskify profile use ${name}`));
|
|
2223
|
+
process.exit(0);
|
|
2224
|
+
});
|
|
2225
|
+
profileCmd
|
|
2226
|
+
.command("use <name>")
|
|
2227
|
+
.description("Switch the active profile")
|
|
2228
|
+
.action(async (name) => {
|
|
2229
|
+
const config = await loadConfig(program.opts().profile);
|
|
2230
|
+
if (!config.profiles[name]) {
|
|
2231
|
+
console.error(chalk.red(`Profile not found: "${name}". Available: ${Object.keys(config.profiles).join(", ")}`));
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
await saveProfiles(name, config.profiles);
|
|
2235
|
+
console.log(chalk.green(`✓ Switched to profile: ${name}`));
|
|
2236
|
+
process.exit(0);
|
|
2237
|
+
});
|
|
2238
|
+
profileCmd
|
|
2239
|
+
.command("show [name]")
|
|
2240
|
+
.description("Show profile details (defaults to active profile)")
|
|
2241
|
+
.action(async (name) => {
|
|
2242
|
+
const config = await loadConfig(program.opts().profile);
|
|
2243
|
+
const profileName = name ?? config.activeProfile;
|
|
2244
|
+
const profile = config.profiles[profileName];
|
|
2245
|
+
if (!profile) {
|
|
2246
|
+
console.error(chalk.red(`Profile not found: "${profileName}". Available: ${Object.keys(config.profiles).join(", ")}`));
|
|
2247
|
+
process.exit(1);
|
|
2248
|
+
}
|
|
2249
|
+
const isActive = profileName === config.activeProfile;
|
|
2250
|
+
console.log(chalk.bold(`Profile: ${profileName}${isActive ? " ◄ active" : ""}`));
|
|
2251
|
+
let npubStr = "(no key)";
|
|
2252
|
+
if (profile.nsec) {
|
|
2253
|
+
const npub = nsecToNpub(profile.nsec);
|
|
2254
|
+
if (npub)
|
|
2255
|
+
npubStr = npub;
|
|
2256
|
+
}
|
|
2257
|
+
const maskedNsec = profile.nsec ? profile.nsec.slice(0, 8) + "..." : "(not set)";
|
|
2258
|
+
console.log(` nsec: ${maskedNsec}`);
|
|
2259
|
+
console.log(` npub: ${npubStr}`);
|
|
2260
|
+
console.log(` relays: ${(profile.relays ?? []).join(", ")}`);
|
|
2261
|
+
console.log(` boards: ${profile.boards?.length ?? 0}`);
|
|
2262
|
+
console.log(` trustedNpubs: ${profile.trustedNpubs?.length ?? 0}`);
|
|
2263
|
+
process.exit(0);
|
|
2264
|
+
});
|
|
2265
|
+
profileCmd
|
|
2266
|
+
.command("remove <name>")
|
|
2267
|
+
.description("Remove a profile")
|
|
2268
|
+
.option("--force", "Skip confirmation prompt")
|
|
2269
|
+
.action(async (name, opts) => {
|
|
2270
|
+
const config = await loadConfig(program.opts().profile);
|
|
2271
|
+
if (!config.profiles[name]) {
|
|
2272
|
+
console.error(chalk.red(`Profile not found: "${name}"`));
|
|
2273
|
+
process.exit(1);
|
|
2274
|
+
}
|
|
2275
|
+
if (name === config.activeProfile) {
|
|
2276
|
+
console.error(chalk.red(`Cannot remove active profile: "${name}". Switch first with: taskify profile use <other>`));
|
|
2277
|
+
process.exit(1);
|
|
2278
|
+
}
|
|
2279
|
+
if (Object.keys(config.profiles).length === 1) {
|
|
2280
|
+
console.error(chalk.red("Cannot remove the only profile."));
|
|
2281
|
+
process.exit(1);
|
|
2282
|
+
}
|
|
2283
|
+
if (!opts.force) {
|
|
2284
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2285
|
+
const confirmed = await new Promise((resolve) => {
|
|
2286
|
+
rl.question(`Remove profile '${name}'? [y/N] `, (ans) => {
|
|
2287
|
+
rl.close();
|
|
2288
|
+
resolve(ans.toLowerCase() === "y");
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
if (!confirmed) {
|
|
2292
|
+
console.log("Aborted.");
|
|
2293
|
+
process.exit(0);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const { [name]: _removed, ...rest } = config.profiles;
|
|
2297
|
+
await saveProfiles(config.activeProfile, rest);
|
|
2298
|
+
console.log(chalk.green(`✓ Profile '${name}' removed.`));
|
|
2299
|
+
process.exit(0);
|
|
2300
|
+
});
|
|
2301
|
+
profileCmd
|
|
2302
|
+
.command("rename <old> <new>")
|
|
2303
|
+
.description("Rename a profile")
|
|
2304
|
+
.action(async (oldName, newName) => {
|
|
2305
|
+
const config = await loadConfig(program.opts().profile);
|
|
2306
|
+
if (!config.profiles[oldName]) {
|
|
2307
|
+
console.error(chalk.red(`Profile not found: "${oldName}"`));
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
if (config.profiles[newName]) {
|
|
2311
|
+
console.error(chalk.red(`Profile already exists: "${newName}"`));
|
|
2312
|
+
process.exit(1);
|
|
2313
|
+
}
|
|
2314
|
+
const { [oldName]: profileData, ...rest } = config.profiles;
|
|
2315
|
+
const newProfiles = { ...rest, [newName]: profileData };
|
|
2316
|
+
const newActive = config.activeProfile === oldName ? newName : config.activeProfile;
|
|
2317
|
+
await saveProfiles(newActive, newProfiles);
|
|
2318
|
+
console.log(chalk.green(`✓ Renamed profile '${oldName}' → '${newName}'`));
|
|
2319
|
+
process.exit(0);
|
|
2320
|
+
});
|
|
2049
2321
|
// ---- setup ----
|
|
2050
2322
|
program
|
|
2051
2323
|
.command("setup")
|
|
2052
|
-
.description("Run the first-run onboarding wizard (re-configure
|
|
2053
|
-
.
|
|
2054
|
-
|
|
2324
|
+
.description("Run the first-run onboarding wizard (re-configure a profile)")
|
|
2325
|
+
.option("--profile <name>", "Profile to configure (defaults to active profile)")
|
|
2326
|
+
.action(async (opts) => {
|
|
2327
|
+
// --profile on setup subcommand takes precedence over global --profile
|
|
2328
|
+
const targetProfile = opts.profile ?? program.opts().profile;
|
|
2329
|
+
const existing = await loadConfig(targetProfile);
|
|
2055
2330
|
if (existing.nsec) {
|
|
2056
2331
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2057
2332
|
const ans = await new Promise((resolve) => {
|
|
2058
|
-
rl.question("
|
|
2333
|
+
rl.question(`⚠ Profile "${existing.activeProfile}" already has a private key. This will replace it.\nContinue? [Y/n] `, resolve);
|
|
2059
2334
|
});
|
|
2060
2335
|
rl.close();
|
|
2061
2336
|
if (ans.trim().toLowerCase() === "n") {
|
|
2062
2337
|
process.exit(0);
|
|
2063
2338
|
}
|
|
2064
2339
|
}
|
|
2065
|
-
await runOnboarding();
|
|
2340
|
+
await runOnboarding(targetProfile ?? existing.activeProfile);
|
|
2066
2341
|
});
|
|
2067
2342
|
// ---- auto-onboarding trigger + parse ----
|
|
2068
|
-
const cfg = await loadConfig();
|
|
2069
|
-
if
|
|
2343
|
+
const cfg = await loadConfig(program.opts().profile);
|
|
2344
|
+
// Trigger onboarding if no profiles have an nsec and no command was given
|
|
2345
|
+
const hasAnyNsec = Object.values(cfg.profiles).some((p) => p.nsec);
|
|
2346
|
+
if (!hasAnyNsec && process.argv.length <= 2) {
|
|
2070
2347
|
await runOnboarding();
|
|
2071
2348
|
}
|
|
2072
2349
|
else {
|
package/dist/nostrRuntime.js
CHANGED
|
@@ -2,7 +2,7 @@ import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRelayStatus } from "@nostr-dev-k
|
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha256";
|
|
3
3
|
import { bytesToHex } from "@noble/hashes/utils";
|
|
4
4
|
import { getPublicKey, nip19 } from "nostr-tools";
|
|
5
|
-
import { saveConfig
|
|
5
|
+
import { saveConfig } from "./config.js";
|
|
6
6
|
import { readCache, writeCache, isCacheFresh } from "./taskCache.js";
|
|
7
7
|
function nowISO() {
|
|
8
8
|
return new Date().toISOString();
|
|
@@ -428,9 +428,8 @@ export function createNostrRuntime(config) {
|
|
|
428
428
|
const fetchPromise = ndk.fetchEvents({ kinds: [30300], "#b": [bTag], limit: 1 }, { closeOnEose: true });
|
|
429
429
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(new Set()), 10000));
|
|
430
430
|
const events = await Promise.race([fetchPromise, timeoutPromise]);
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
const entry = cfg.boards.find((b) => b.id === boardId);
|
|
431
|
+
// Use config from closure (avoids extra file read and ensures correct profile)
|
|
432
|
+
const entry = config.boards.find((b) => b.id === boardId);
|
|
434
433
|
if (!entry)
|
|
435
434
|
return {};
|
|
436
435
|
let kind;
|
|
@@ -494,7 +493,7 @@ export function createNostrRuntime(config) {
|
|
|
494
493
|
entry.columns = columns;
|
|
495
494
|
if (children && children.length > 0)
|
|
496
495
|
entry.children = children;
|
|
497
|
-
await saveConfig(
|
|
496
|
+
await saveConfig(config);
|
|
498
497
|
}
|
|
499
498
|
return { kind, columns, children };
|
|
500
499
|
},
|
|
@@ -802,31 +801,28 @@ export function createNostrRuntime(config) {
|
|
|
802
801
|
},
|
|
803
802
|
async remindTask(taskId, presets) {
|
|
804
803
|
// Device-local only — NEVER publish to Nostr
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
await saveConfig(cfg);
|
|
804
|
+
if (!config.taskReminders)
|
|
805
|
+
config.taskReminders = {};
|
|
806
|
+
config.taskReminders[taskId] = presets;
|
|
807
|
+
await saveConfig(config);
|
|
810
808
|
process.stderr.write("\x1b[2m Note: Reminders are device-local and will not sync to other devices\x1b[0m\n");
|
|
811
809
|
},
|
|
812
810
|
getLocalReminders(taskId) {
|
|
813
811
|
return config.taskReminders?.[taskId] ?? [];
|
|
814
812
|
},
|
|
815
813
|
async getAgentSecurityConfig() {
|
|
816
|
-
const cfg = await loadConfig();
|
|
817
814
|
return {
|
|
818
|
-
enabled:
|
|
819
|
-
mode:
|
|
820
|
-
trustedNpubs:
|
|
815
|
+
enabled: config.securityEnabled,
|
|
816
|
+
mode: config.securityMode,
|
|
817
|
+
trustedNpubs: config.trustedNpubs,
|
|
821
818
|
updatedISO: nowISO(),
|
|
822
819
|
};
|
|
823
820
|
},
|
|
824
821
|
async setAgentSecurityConfig(secCfg) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
await saveConfig(cfg);
|
|
822
|
+
config.securityEnabled = secCfg.enabled;
|
|
823
|
+
config.securityMode = secCfg.mode;
|
|
824
|
+
config.trustedNpubs = secCfg.trustedNpubs;
|
|
825
|
+
await saveConfig(config);
|
|
830
826
|
return secCfg;
|
|
831
827
|
},
|
|
832
828
|
async getRelayStatus() {
|
|
@@ -873,15 +869,14 @@ export function createNostrRuntime(config) {
|
|
|
873
869
|
throw new Error(`Board publish failed: ${String(err)}`);
|
|
874
870
|
}
|
|
875
871
|
// Auto-join: save to config
|
|
876
|
-
const cfg = await loadConfig();
|
|
877
872
|
const newEntry = {
|
|
878
873
|
id: boardId,
|
|
879
874
|
name: input.name,
|
|
880
875
|
kind: input.kind,
|
|
881
876
|
columns: input.columns ?? [],
|
|
882
877
|
};
|
|
883
|
-
|
|
884
|
-
await saveConfig(
|
|
878
|
+
config.boards.push(newEntry);
|
|
879
|
+
await saveConfig(config);
|
|
885
880
|
return { boardId };
|
|
886
881
|
},
|
|
887
882
|
};
|
package/dist/onboarding.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as readline from "readline";
|
|
3
3
|
import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
|
|
4
|
-
import { loadConfig,
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { loadConfig, saveProfiles } from "./config.js";
|
|
5
|
+
// Queue-based readline helper that works correctly with piped stdin
|
|
6
|
+
function makeLineQueue(rl) {
|
|
7
|
+
const lineQueue = [];
|
|
8
|
+
const waiters = [];
|
|
9
|
+
rl.on("line", (line) => {
|
|
10
|
+
if (waiters.length > 0) {
|
|
11
|
+
waiters.shift()(line);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
lineQueue.push(line);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return (prompt) => {
|
|
18
|
+
process.stdout.write(prompt);
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
if (lineQueue.length > 0) {
|
|
21
|
+
resolve(lineQueue.shift());
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
waiters.push(resolve);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
7
28
|
}
|
|
8
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Run the onboarding wizard.
|
|
31
|
+
* @param profileName - If provided, save to this profile name (re-configure).
|
|
32
|
+
* If not provided, ask the user for a profile name.
|
|
33
|
+
*/
|
|
34
|
+
export async function runOnboarding(profileName) {
|
|
9
35
|
console.log();
|
|
10
36
|
console.log("┌─────────────────────────────────────────┐");
|
|
11
37
|
console.log("│ Welcome to taskify-nostr! 🦉 │");
|
|
@@ -16,18 +42,26 @@ export async function runOnboarding() {
|
|
|
16
42
|
input: process.stdin,
|
|
17
43
|
output: process.stdout,
|
|
18
44
|
});
|
|
19
|
-
const
|
|
45
|
+
const ask = makeLineQueue(rl);
|
|
46
|
+
const DEFAULT_RELAYS = [
|
|
47
|
+
"wss://relay.damus.io",
|
|
48
|
+
"wss://nos.lol",
|
|
49
|
+
"wss://relay.snort.social",
|
|
50
|
+
"wss://relay.primal.net",
|
|
51
|
+
];
|
|
52
|
+
let nsec;
|
|
53
|
+
let relays = [...DEFAULT_RELAYS];
|
|
20
54
|
// Step 1 — Private key
|
|
21
55
|
console.log("Step 1 — Private key");
|
|
22
|
-
const hasKey = await ask(
|
|
56
|
+
const hasKey = await ask("Do you have a Nostr private key (nsec)? [Y/n] ");
|
|
23
57
|
if (hasKey.trim().toLowerCase() !== "n") {
|
|
24
58
|
// User has a key
|
|
25
|
-
let nsec = "";
|
|
26
59
|
while (true) {
|
|
27
|
-
|
|
28
|
-
if (
|
|
60
|
+
const input = (await ask("Paste your nsec: ")).trim();
|
|
61
|
+
if (input.startsWith("nsec1")) {
|
|
29
62
|
try {
|
|
30
|
-
nip19.decode(
|
|
63
|
+
nip19.decode(input);
|
|
64
|
+
nsec = input;
|
|
31
65
|
break;
|
|
32
66
|
}
|
|
33
67
|
catch {
|
|
@@ -36,13 +70,12 @@ export async function runOnboarding() {
|
|
|
36
70
|
}
|
|
37
71
|
console.log("Invalid nsec. Try again or press Ctrl+C to abort.");
|
|
38
72
|
}
|
|
39
|
-
cfg.nsec = nsec;
|
|
40
73
|
}
|
|
41
74
|
else {
|
|
42
75
|
// Generate new keypair
|
|
43
76
|
const sk = generateSecretKey();
|
|
44
77
|
const pk = getPublicKey(sk);
|
|
45
|
-
|
|
78
|
+
nsec = nip19.nsecEncode(sk);
|
|
46
79
|
const npub = nip19.npubEncode(pk);
|
|
47
80
|
console.log();
|
|
48
81
|
console.log("✓ Generated new Nostr identity");
|
|
@@ -50,44 +83,68 @@ export async function runOnboarding() {
|
|
|
50
83
|
console.log(` nsec: ${nsec} ← KEEP THIS SECRET — it is your password`);
|
|
51
84
|
console.log();
|
|
52
85
|
console.log("Save this nsec somewhere safe. It cannot be recovered if lost.");
|
|
53
|
-
const cont = await ask(
|
|
86
|
+
const cont = await ask("Continue? [Y/n] ");
|
|
54
87
|
if (cont.trim().toLowerCase() === "n") {
|
|
55
88
|
rl.close();
|
|
56
89
|
process.exit(0);
|
|
57
90
|
}
|
|
58
|
-
cfg.nsec = nsec;
|
|
59
91
|
}
|
|
60
92
|
// Step 2 — Default board
|
|
61
93
|
console.log();
|
|
62
94
|
console.log("Step 2 — Default board");
|
|
63
|
-
const joinBoard = await ask(
|
|
95
|
+
const joinBoard = await ask("Do you want to join an existing board? [y/N] ");
|
|
96
|
+
let defaultBoard = "Personal";
|
|
64
97
|
if (joinBoard.trim().toLowerCase() === "y") {
|
|
65
|
-
const boardId = (await ask(
|
|
98
|
+
const boardId = (await ask("Board ID (Nostr event id): ")).trim();
|
|
66
99
|
if (boardId) {
|
|
67
|
-
|
|
100
|
+
defaultBoard = boardId;
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
103
|
// Step 3 — Relays
|
|
71
104
|
console.log();
|
|
72
105
|
console.log("Step 3 — Relays");
|
|
73
|
-
const configRelays = await ask(
|
|
106
|
+
const configRelays = await ask("Configure relays? Default relays will be used if skipped. [y/N] ");
|
|
74
107
|
if (configRelays.trim().toLowerCase() === "y") {
|
|
75
|
-
const
|
|
108
|
+
const customRelays = [];
|
|
76
109
|
while (true) {
|
|
77
|
-
const relay = (await ask(
|
|
110
|
+
const relay = (await ask("Add relay URL (blank to finish): ")).trim();
|
|
78
111
|
if (!relay)
|
|
79
112
|
break;
|
|
80
|
-
|
|
113
|
+
customRelays.push(relay);
|
|
81
114
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
115
|
+
if (customRelays.length > 0) {
|
|
116
|
+
relays = customRelays;
|
|
84
117
|
}
|
|
85
118
|
}
|
|
119
|
+
// Step 4 — Profile name (only when not re-configuring an existing profile)
|
|
120
|
+
let resolvedProfileName = profileName;
|
|
121
|
+
if (!resolvedProfileName) {
|
|
122
|
+
console.log();
|
|
123
|
+
console.log("Step 4 — Profile name");
|
|
124
|
+
const nameInput = (await ask("What should we name this profile? [default] ")).trim();
|
|
125
|
+
resolvedProfileName = nameInput || "default";
|
|
126
|
+
}
|
|
86
127
|
rl.close();
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
// Load full config to preserve other profiles
|
|
129
|
+
const fullCfg = await loadConfig();
|
|
130
|
+
const existingProfile = fullCfg.profiles[resolvedProfileName];
|
|
131
|
+
const newProfile = {
|
|
132
|
+
nsec,
|
|
133
|
+
relays,
|
|
134
|
+
boards: existingProfile?.boards ?? [],
|
|
135
|
+
trustedNpubs: existingProfile?.trustedNpubs ?? [],
|
|
136
|
+
securityMode: existingProfile?.securityMode ?? "moderate",
|
|
137
|
+
securityEnabled: existingProfile?.securityEnabled ?? true,
|
|
138
|
+
defaultBoard,
|
|
139
|
+
taskReminders: existingProfile?.taskReminders ?? {},
|
|
140
|
+
agent: existingProfile?.agent,
|
|
141
|
+
};
|
|
142
|
+
const newProfiles = { ...fullCfg.profiles, [resolvedProfileName]: newProfile };
|
|
143
|
+
await saveProfiles(resolvedProfileName, newProfiles);
|
|
144
|
+
// Done
|
|
89
145
|
console.log();
|
|
90
|
-
console.log(
|
|
146
|
+
console.log(`✓ Setup complete! Profile: "${resolvedProfileName}"`);
|
|
147
|
+
console.log(" Run `taskify boards` to see your boards.");
|
|
91
148
|
console.log(" Run `taskify --help` to explore all commands.");
|
|
92
149
|
console.log();
|
|
93
150
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskify-nostr",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Nostr-powered task management CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"taskify": "
|
|
7
|
+
"taskify": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc && node -e \"const fs=require('fs');const f='dist/index.js';let c=fs.readFileSync(f,'utf8');if(c.startsWith('#!'))c=c.slice(c.indexOf('\\n')+1);fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c)\"",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/Solife-me/Taskify_Release"
|
|
31
|
+
"url": "git+https://github.com/Solife-me/Taskify_Release.git"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@noble/hashes": "^1.4.0",
|