taskify-nostr 0.1.0 → 0.2.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 +44 -0
- package/dist/config.js +96 -14
- package/dist/index.js +294 -53
- 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,8 +3,8 @@ 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 { nip19 } from "nostr-tools";
|
|
7
|
-
import { loadConfig, saveConfig } from "./config.js";
|
|
6
|
+
import { nip19, getPublicKey, generateSecretKey } from "nostr-tools";
|
|
7
|
+
import { loadConfig, saveConfig, saveProfiles, DEFAULT_RELAYS } from "./config.js";
|
|
8
8
|
import { createNostrRuntime } from "./nostrRuntime.js";
|
|
9
9
|
import { renderTable, renderTaskCard, renderJson } from "./render.js";
|
|
10
10
|
import { zshCompletion, bashCompletion, fishCompletion } from "./completions.js";
|
|
@@ -14,7 +14,8 @@ const program = new Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name("taskify")
|
|
16
16
|
.version("0.1.0")
|
|
17
|
-
.description("Taskify CLI — manage tasks over Nostr")
|
|
17
|
+
.description("Taskify CLI — manage tasks over Nostr")
|
|
18
|
+
.option("-P, --profile <name>", "Use a specific profile for this command (does not change active profile)");
|
|
18
19
|
// ---- Validation helpers ----
|
|
19
20
|
function validateDue(due) {
|
|
20
21
|
if (!due)
|
|
@@ -87,7 +88,7 @@ boardCmd
|
|
|
87
88
|
.command("list")
|
|
88
89
|
.description("List all configured boards")
|
|
89
90
|
.action(async () => {
|
|
90
|
-
const config = await loadConfig();
|
|
91
|
+
const config = await loadConfig(program.opts().profile);
|
|
91
92
|
if (config.boards.length === 0) {
|
|
92
93
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
93
94
|
}
|
|
@@ -109,7 +110,7 @@ boardCmd
|
|
|
109
110
|
if (!UUID_RE.test(boardId)) {
|
|
110
111
|
console.warn(chalk.yellow(`Warning: "${boardId}" does not look like a UUID.`));
|
|
111
112
|
}
|
|
112
|
-
const config = await loadConfig();
|
|
113
|
+
const config = await loadConfig(program.opts().profile);
|
|
113
114
|
const existing = config.boards.find((b) => b.id === boardId);
|
|
114
115
|
if (existing) {
|
|
115
116
|
console.log(chalk.dim(`Already on board ${existing.name} (${boardId})`));
|
|
@@ -140,7 +141,7 @@ boardCmd
|
|
|
140
141
|
.command("sync [boardId]")
|
|
141
142
|
.description("Sync board metadata (kind, columns) from Nostr")
|
|
142
143
|
.action(async (boardId) => {
|
|
143
|
-
const config = await loadConfig();
|
|
144
|
+
const config = await loadConfig(program.opts().profile);
|
|
144
145
|
if (config.boards.length === 0) {
|
|
145
146
|
console.error(chalk.red("No boards configured."));
|
|
146
147
|
process.exit(1);
|
|
@@ -164,7 +165,7 @@ boardCmd
|
|
|
164
165
|
const meta = await runtime.syncBoard(entry.id);
|
|
165
166
|
const colCount = meta.columns?.length ?? 0;
|
|
166
167
|
const kindStr = meta.kind ?? "unknown";
|
|
167
|
-
const reloadedEntry = (await loadConfig()).boards.find((b) => b.id === entry.id);
|
|
168
|
+
const reloadedEntry = (await loadConfig(program.opts().profile)).boards.find((b) => b.id === entry.id);
|
|
168
169
|
const childrenCount = reloadedEntry?.children?.length ?? 0;
|
|
169
170
|
const childrenStr = kindStr === "compound" ? `, children: ${childrenCount}` : "";
|
|
170
171
|
console.log(chalk.green(`✓ Synced: ${entry.name} (kind: ${kindStr}, columns: ${colCount}${childrenStr})`));
|
|
@@ -184,7 +185,7 @@ boardCmd
|
|
|
184
185
|
.command("leave <boardId>")
|
|
185
186
|
.description("Remove a board from config")
|
|
186
187
|
.action(async (boardId) => {
|
|
187
|
-
const config = await loadConfig();
|
|
188
|
+
const config = await loadConfig(program.opts().profile);
|
|
188
189
|
const before = config.boards.length;
|
|
189
190
|
config.boards = config.boards.filter((b) => b.id !== boardId);
|
|
190
191
|
if (config.boards.length === before) {
|
|
@@ -199,7 +200,7 @@ boardCmd
|
|
|
199
200
|
.command("columns")
|
|
200
201
|
.description("Show cached columns for all configured boards")
|
|
201
202
|
.action(async () => {
|
|
202
|
-
const config = await loadConfig();
|
|
203
|
+
const config = await loadConfig(program.opts().profile);
|
|
203
204
|
if (config.boards.length === 0) {
|
|
204
205
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
205
206
|
process.exit(0);
|
|
@@ -222,7 +223,7 @@ boardCmd
|
|
|
222
223
|
.command("children <board>")
|
|
223
224
|
.description("List children of a compound board")
|
|
224
225
|
.action(async (boardArg) => {
|
|
225
|
-
const config = await loadConfig();
|
|
226
|
+
const config = await loadConfig(program.opts().profile);
|
|
226
227
|
const entry = config.boards.find((b) => b.id === boardArg) ??
|
|
227
228
|
config.boards.find((b) => b.name.toLowerCase() === boardArg.toLowerCase());
|
|
228
229
|
if (!entry) {
|
|
@@ -254,7 +255,7 @@ program
|
|
|
254
255
|
.command("boards")
|
|
255
256
|
.description("List configured boards (alias for: board list)")
|
|
256
257
|
.action(async () => {
|
|
257
|
-
const config = await loadConfig();
|
|
258
|
+
const config = await loadConfig(program.opts().profile);
|
|
258
259
|
if (config.boards.length === 0) {
|
|
259
260
|
console.log(chalk.dim("No boards configured. Use: taskify board join <id> --name <name>"));
|
|
260
261
|
}
|
|
@@ -318,7 +319,7 @@ program
|
|
|
318
319
|
.option("--no-cache", "Do not fall back to stale cache if relay returns empty")
|
|
319
320
|
.option("--json", "Output as JSON")
|
|
320
321
|
.action(async (opts) => {
|
|
321
|
-
const config = await loadConfig();
|
|
322
|
+
const config = await loadConfig(program.opts().profile);
|
|
322
323
|
const runtime = initRuntime(config);
|
|
323
324
|
let exitCode = 0;
|
|
324
325
|
try {
|
|
@@ -380,7 +381,7 @@ program
|
|
|
380
381
|
.option("--json", "Output raw task fields as JSON")
|
|
381
382
|
.action(async (taskId, opts) => {
|
|
382
383
|
warnShortTaskId(taskId);
|
|
383
|
-
const config = await loadConfig();
|
|
384
|
+
const config = await loadConfig(program.opts().profile);
|
|
384
385
|
const runtime = initRuntime(config);
|
|
385
386
|
let exitCode = 0;
|
|
386
387
|
try {
|
|
@@ -413,7 +414,7 @@ program
|
|
|
413
414
|
.option("--board <id|name>", "Limit to a specific board")
|
|
414
415
|
.option("--json", "Output as JSON")
|
|
415
416
|
.action(async (query, opts) => {
|
|
416
|
-
const config = await loadConfig();
|
|
417
|
+
const config = await loadConfig(program.opts().profile);
|
|
417
418
|
const runtime = initRuntime(config);
|
|
418
419
|
let exitCode = 0;
|
|
419
420
|
try {
|
|
@@ -460,7 +461,7 @@ program
|
|
|
460
461
|
console.error(chalk.red(`Invalid reminder preset(s): ${invalid.join(", ")}. Valid: ${[...VALID_REMINDER_PRESETS].join(", ")}`));
|
|
461
462
|
process.exit(1);
|
|
462
463
|
}
|
|
463
|
-
const config = await loadConfig();
|
|
464
|
+
const config = await loadConfig(program.opts().profile);
|
|
464
465
|
const runtime = initRuntime(config);
|
|
465
466
|
let exitCode = 0;
|
|
466
467
|
try {
|
|
@@ -502,7 +503,7 @@ program
|
|
|
502
503
|
.action(async (title, opts) => {
|
|
503
504
|
validateDue(opts.due);
|
|
504
505
|
validatePriority(opts.priority);
|
|
505
|
-
const config = await loadConfig();
|
|
506
|
+
const config = await loadConfig(program.opts().profile);
|
|
506
507
|
const boardId = await resolveBoardId(opts.board, config);
|
|
507
508
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
508
509
|
// Block add on compound boards
|
|
@@ -576,7 +577,7 @@ program
|
|
|
576
577
|
.option("--json", "Output updated task as JSON")
|
|
577
578
|
.action(async (taskId, opts) => {
|
|
578
579
|
warnShortTaskId(taskId);
|
|
579
|
-
const config = await loadConfig();
|
|
580
|
+
const config = await loadConfig(program.opts().profile);
|
|
580
581
|
const boardId = await resolveBoardId(opts.board, config);
|
|
581
582
|
const runtime = initRuntime(config);
|
|
582
583
|
let exitCode = 0;
|
|
@@ -610,7 +611,7 @@ program
|
|
|
610
611
|
.option("--json", "Output updated task as JSON")
|
|
611
612
|
.action(async (taskId, opts) => {
|
|
612
613
|
warnShortTaskId(taskId);
|
|
613
|
-
const config = await loadConfig();
|
|
614
|
+
const config = await loadConfig(program.opts().profile);
|
|
614
615
|
const boardId = await resolveBoardId(opts.board, config);
|
|
615
616
|
const runtime = initRuntime(config);
|
|
616
617
|
let exitCode = 0;
|
|
@@ -645,7 +646,7 @@ program
|
|
|
645
646
|
.option("--json", "Output deleted task as JSON")
|
|
646
647
|
.action(async (taskId, opts) => {
|
|
647
648
|
warnShortTaskId(taskId);
|
|
648
|
-
const config = await loadConfig();
|
|
649
|
+
const config = await loadConfig(program.opts().profile);
|
|
649
650
|
const boardId = await resolveBoardId(opts.board, config);
|
|
650
651
|
const runtime = initRuntime(config);
|
|
651
652
|
let exitCode = 0;
|
|
@@ -712,7 +713,7 @@ program
|
|
|
712
713
|
process.exit(1);
|
|
713
714
|
}
|
|
714
715
|
warnShortTaskId(taskId);
|
|
715
|
-
const config = await loadConfig();
|
|
716
|
+
const config = await loadConfig(program.opts().profile);
|
|
716
717
|
const boardId = await resolveBoardId(opts.board, config);
|
|
717
718
|
const runtime = initRuntime(config);
|
|
718
719
|
let exitCode = 0;
|
|
@@ -767,7 +768,7 @@ program
|
|
|
767
768
|
warnShortTaskId(taskId);
|
|
768
769
|
validateDue(opts.due);
|
|
769
770
|
validatePriority(opts.priority);
|
|
770
|
-
const config = await loadConfig();
|
|
771
|
+
const config = await loadConfig(program.opts().profile);
|
|
771
772
|
const boardId = await resolveBoardId(opts.board, config);
|
|
772
773
|
const runtime = initRuntime(config);
|
|
773
774
|
let exitCode = 0;
|
|
@@ -820,7 +821,7 @@ trust
|
|
|
820
821
|
.command("add <npub>")
|
|
821
822
|
.description("Add a trusted npub")
|
|
822
823
|
.action(async (npub) => {
|
|
823
|
-
const config = await loadConfig();
|
|
824
|
+
const config = await loadConfig(program.opts().profile);
|
|
824
825
|
if (!config.trustedNpubs.includes(npub)) {
|
|
825
826
|
config.trustedNpubs.push(npub);
|
|
826
827
|
}
|
|
@@ -832,7 +833,7 @@ trust
|
|
|
832
833
|
.command("remove <npub>")
|
|
833
834
|
.description("Remove a trusted npub")
|
|
834
835
|
.action(async (npub) => {
|
|
835
|
-
const config = await loadConfig();
|
|
836
|
+
const config = await loadConfig(program.opts().profile);
|
|
836
837
|
config.trustedNpubs = config.trustedNpubs.filter((n) => n !== npub);
|
|
837
838
|
await saveConfig(config);
|
|
838
839
|
console.log(chalk.green("✓ Removed"));
|
|
@@ -842,7 +843,7 @@ trust
|
|
|
842
843
|
.command("list")
|
|
843
844
|
.description("List trusted npubs")
|
|
844
845
|
.action(async () => {
|
|
845
|
-
const config = await loadConfig();
|
|
846
|
+
const config = await loadConfig(program.opts().profile);
|
|
846
847
|
if (config.trustedNpubs.length === 0) {
|
|
847
848
|
console.log(chalk.dim("No trusted npubs."));
|
|
848
849
|
}
|
|
@@ -859,7 +860,7 @@ relayCmd
|
|
|
859
860
|
.command("status")
|
|
860
861
|
.description("Show connection status of relays in the NDK pool")
|
|
861
862
|
.action(async () => {
|
|
862
|
-
const config = await loadConfig();
|
|
863
|
+
const config = await loadConfig(program.opts().profile);
|
|
863
864
|
const runtime = initRuntime(config);
|
|
864
865
|
let exitCode = 0;
|
|
865
866
|
try {
|
|
@@ -891,7 +892,7 @@ relayCmd
|
|
|
891
892
|
.command("list")
|
|
892
893
|
.description("Show configured relays with live connection check")
|
|
893
894
|
.action(async () => {
|
|
894
|
-
const config = await loadConfig();
|
|
895
|
+
const config = await loadConfig(program.opts().profile);
|
|
895
896
|
if (config.relays.length === 0) {
|
|
896
897
|
console.log(chalk.dim("No relays configured."));
|
|
897
898
|
process.exit(0);
|
|
@@ -912,7 +913,7 @@ relayCmd
|
|
|
912
913
|
.command("add <url>")
|
|
913
914
|
.description("Add a relay URL to config")
|
|
914
915
|
.action(async (url) => {
|
|
915
|
-
const config = await loadConfig();
|
|
916
|
+
const config = await loadConfig(program.opts().profile);
|
|
916
917
|
if (!config.relays.includes(url)) {
|
|
917
918
|
config.relays.push(url);
|
|
918
919
|
await saveConfig(config);
|
|
@@ -927,7 +928,7 @@ relayCmd
|
|
|
927
928
|
.command("remove <url>")
|
|
928
929
|
.description("Remove a relay URL from config")
|
|
929
930
|
.action(async (url) => {
|
|
930
|
-
const config = await loadConfig();
|
|
931
|
+
const config = await loadConfig(program.opts().profile);
|
|
931
932
|
const before = config.relays.length;
|
|
932
933
|
config.relays = config.relays.filter((r) => r !== url);
|
|
933
934
|
if (config.relays.length === before) {
|
|
@@ -952,7 +953,7 @@ cacheCmd
|
|
|
952
953
|
.command("status")
|
|
953
954
|
.description("Show per-board cache age and task count")
|
|
954
955
|
.action(async () => {
|
|
955
|
-
const config = await loadConfig();
|
|
956
|
+
const config = await loadConfig(program.opts().profile);
|
|
956
957
|
const cache = readCache();
|
|
957
958
|
const now = Date.now();
|
|
958
959
|
if (Object.keys(cache.boards).length === 0) {
|
|
@@ -1000,7 +1001,7 @@ configSet
|
|
|
1000
1001
|
console.error(chalk.red(`Invalid nsec: must start with "nsec1".`));
|
|
1001
1002
|
process.exit(1);
|
|
1002
1003
|
}
|
|
1003
|
-
const config = await loadConfig();
|
|
1004
|
+
const config = await loadConfig(program.opts().profile);
|
|
1004
1005
|
config.nsec = nsec;
|
|
1005
1006
|
await saveConfig(config);
|
|
1006
1007
|
console.log(chalk.green("✓ nsec saved"));
|
|
@@ -1010,7 +1011,7 @@ configSet
|
|
|
1010
1011
|
.command("relay <url>")
|
|
1011
1012
|
.description("Add a relay URL")
|
|
1012
1013
|
.action(async (url) => {
|
|
1013
|
-
const config = await loadConfig();
|
|
1014
|
+
const config = await loadConfig(program.opts().profile);
|
|
1014
1015
|
if (!config.relays.includes(url)) {
|
|
1015
1016
|
config.relays.push(url);
|
|
1016
1017
|
}
|
|
@@ -1054,7 +1055,7 @@ configCmd
|
|
|
1054
1055
|
.command("show")
|
|
1055
1056
|
.description("Show current config")
|
|
1056
1057
|
.action(async () => {
|
|
1057
|
-
const config = await loadConfig();
|
|
1058
|
+
const config = await loadConfig(program.opts().profile);
|
|
1058
1059
|
const display = {
|
|
1059
1060
|
...config,
|
|
1060
1061
|
nsec: config.nsec ? "nsec1****" : undefined,
|
|
@@ -1122,7 +1123,7 @@ agentConfigCmd
|
|
|
1122
1123
|
.command("set-key <key>")
|
|
1123
1124
|
.description("Set the AI API key")
|
|
1124
1125
|
.action(async (key) => {
|
|
1125
|
-
const config = await loadConfig();
|
|
1126
|
+
const config = await loadConfig(program.opts().profile);
|
|
1126
1127
|
if (!config.agent)
|
|
1127
1128
|
config.agent = {};
|
|
1128
1129
|
config.agent.apiKey = key;
|
|
@@ -1134,7 +1135,7 @@ agentConfigCmd
|
|
|
1134
1135
|
.command("set-model <model>")
|
|
1135
1136
|
.description("Set the AI model")
|
|
1136
1137
|
.action(async (model) => {
|
|
1137
|
-
const config = await loadConfig();
|
|
1138
|
+
const config = await loadConfig(program.opts().profile);
|
|
1138
1139
|
if (!config.agent)
|
|
1139
1140
|
config.agent = {};
|
|
1140
1141
|
config.agent.model = model;
|
|
@@ -1146,7 +1147,7 @@ agentConfigCmd
|
|
|
1146
1147
|
.command("set-url <url>")
|
|
1147
1148
|
.description("Set the AI base URL (OpenAI-compatible)")
|
|
1148
1149
|
.action(async (url) => {
|
|
1149
|
-
const config = await loadConfig();
|
|
1150
|
+
const config = await loadConfig(program.opts().profile);
|
|
1150
1151
|
if (!config.agent)
|
|
1151
1152
|
config.agent = {};
|
|
1152
1153
|
config.agent.baseUrl = url;
|
|
@@ -1158,7 +1159,7 @@ agentConfigCmd
|
|
|
1158
1159
|
.command("show")
|
|
1159
1160
|
.description("Show current agent config (masks API key)")
|
|
1160
1161
|
.action(async () => {
|
|
1161
|
-
const config = await loadConfig();
|
|
1162
|
+
const config = await loadConfig(program.opts().profile);
|
|
1162
1163
|
const ag = config.agent ?? {};
|
|
1163
1164
|
const rawKey = ag.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1164
1165
|
let maskedKey = "(not set)";
|
|
@@ -1182,7 +1183,7 @@ agentCmd
|
|
|
1182
1183
|
.option("--dry-run", "Show extracted fields without creating")
|
|
1183
1184
|
.option("--json", "Output created task as JSON")
|
|
1184
1185
|
.action(async (description, opts) => {
|
|
1185
|
-
const config = await loadConfig();
|
|
1186
|
+
const config = await loadConfig(program.opts().profile);
|
|
1186
1187
|
const apiKey = config.agent?.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1187
1188
|
if (!apiKey) {
|
|
1188
1189
|
console.error(chalk.red("No AI API key configured. Run: taskify agent config set-key <key>"));
|
|
@@ -1311,7 +1312,7 @@ agentCmd
|
|
|
1311
1312
|
.option("--dry-run", "Show suggestions without applying")
|
|
1312
1313
|
.option("--json", "Output suggestions as JSON")
|
|
1313
1314
|
.action(async (opts) => {
|
|
1314
|
-
const config = await loadConfig();
|
|
1315
|
+
const config = await loadConfig(program.opts().profile);
|
|
1315
1316
|
const apiKey = config.agent?.apiKey ?? process.env.TASKIFY_AGENT_API_KEY ?? "";
|
|
1316
1317
|
if (!apiKey) {
|
|
1317
1318
|
console.error(chalk.red("No AI API key configured. Run: taskify agent config set-key <key>"));
|
|
@@ -1491,7 +1492,7 @@ program
|
|
|
1491
1492
|
.option("--status <open|done|any>", "Status filter (default: open)", "open")
|
|
1492
1493
|
.option("--output <file>", "Write to file instead of stdout")
|
|
1493
1494
|
.action(async (opts) => {
|
|
1494
|
-
const config = await loadConfig();
|
|
1495
|
+
const config = await loadConfig(program.opts().profile);
|
|
1495
1496
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1496
1497
|
const runtime = initRuntime(config);
|
|
1497
1498
|
let exitCode = 0;
|
|
@@ -1596,7 +1597,7 @@ program
|
|
|
1596
1597
|
.option("--dry-run", "Print preview but do not create tasks")
|
|
1597
1598
|
.option("--yes", "Skip confirmation prompt")
|
|
1598
1599
|
.action(async (file, opts) => {
|
|
1599
|
-
const config = await loadConfig();
|
|
1600
|
+
const config = await loadConfig(program.opts().profile);
|
|
1600
1601
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1601
1602
|
let raw;
|
|
1602
1603
|
try {
|
|
@@ -1743,7 +1744,7 @@ inboxCmd
|
|
|
1743
1744
|
.description("List inbox tasks (inboxItem: true)")
|
|
1744
1745
|
.option("--board <id|name>", "Board to list from")
|
|
1745
1746
|
.action(async (opts) => {
|
|
1746
|
-
const config = await loadConfig();
|
|
1747
|
+
const config = await loadConfig(program.opts().profile);
|
|
1747
1748
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1748
1749
|
const runtime = initRuntime(config);
|
|
1749
1750
|
let exitCode = 0;
|
|
@@ -1771,7 +1772,7 @@ inboxCmd
|
|
|
1771
1772
|
.description("Capture a task to inbox (inboxItem: true)")
|
|
1772
1773
|
.option("--board <id|name>", "Board to add to")
|
|
1773
1774
|
.action(async (title, opts) => {
|
|
1774
|
-
const config = await loadConfig();
|
|
1775
|
+
const config = await loadConfig(program.opts().profile);
|
|
1775
1776
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1776
1777
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
1777
1778
|
if (boardEntry.kind === "compound") {
|
|
@@ -1810,7 +1811,7 @@ inboxCmd
|
|
|
1810
1811
|
validateDue(opts.due);
|
|
1811
1812
|
validatePriority(opts.priority);
|
|
1812
1813
|
warnShortTaskId(taskId);
|
|
1813
|
-
const config = await loadConfig();
|
|
1814
|
+
const config = await loadConfig(program.opts().profile);
|
|
1814
1815
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1815
1816
|
const boardEntry = config.boards.find((b) => b.id === boardId);
|
|
1816
1817
|
const runtime = initRuntime(config);
|
|
@@ -1925,7 +1926,7 @@ boardCmd
|
|
|
1925
1926
|
process.exit(1);
|
|
1926
1927
|
}
|
|
1927
1928
|
const kind = opts.kind;
|
|
1928
|
-
const config = await loadConfig();
|
|
1929
|
+
const config = await loadConfig(program.opts().profile);
|
|
1929
1930
|
const runtime = initRuntime(config);
|
|
1930
1931
|
let exitCode = 0;
|
|
1931
1932
|
try {
|
|
@@ -1967,7 +1968,7 @@ program
|
|
|
1967
1968
|
.action(async (taskId, npubOrHex, opts) => {
|
|
1968
1969
|
warnShortTaskId(taskId);
|
|
1969
1970
|
const hex = npubOrHexToHex(npubOrHex);
|
|
1970
|
-
const config = await loadConfig();
|
|
1971
|
+
const config = await loadConfig(program.opts().profile);
|
|
1971
1972
|
const boardId = await resolveBoardId(opts.board, config);
|
|
1972
1973
|
const runtime = initRuntime(config);
|
|
1973
1974
|
let exitCode = 0;
|
|
@@ -2013,7 +2014,7 @@ program
|
|
|
2013
2014
|
.action(async (taskId, npubOrHex, opts) => {
|
|
2014
2015
|
warnShortTaskId(taskId);
|
|
2015
2016
|
const hex = npubOrHexToHex(npubOrHex);
|
|
2016
|
-
const config = await loadConfig();
|
|
2017
|
+
const config = await loadConfig(program.opts().profile);
|
|
2017
2018
|
const boardId = await resolveBoardId(opts.board, config);
|
|
2018
2019
|
const runtime = initRuntime(config);
|
|
2019
2020
|
let exitCode = 0;
|
|
@@ -2046,27 +2047,267 @@ program
|
|
|
2046
2047
|
process.exit(exitCode);
|
|
2047
2048
|
}
|
|
2048
2049
|
});
|
|
2050
|
+
// ---- Helper: readline queue (handles piped stdin correctly) ----
|
|
2051
|
+
function makeLineQueue(rl) {
|
|
2052
|
+
const lineQueue = [];
|
|
2053
|
+
const waiters = [];
|
|
2054
|
+
rl.on("line", (line) => {
|
|
2055
|
+
if (waiters.length > 0) {
|
|
2056
|
+
waiters.shift()(line);
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
lineQueue.push(line);
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
return (prompt) => {
|
|
2063
|
+
process.stdout.write(prompt);
|
|
2064
|
+
return new Promise((resolve) => {
|
|
2065
|
+
if (lineQueue.length > 0) {
|
|
2066
|
+
resolve(lineQueue.shift());
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
waiters.push(resolve);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
// ---- profile command group ----
|
|
2075
|
+
const profileCmd = program
|
|
2076
|
+
.command("profile")
|
|
2077
|
+
.description("Manage named Nostr identity profiles");
|
|
2078
|
+
// Helper to get npub string from nsec
|
|
2079
|
+
function nsecToNpub(nsec) {
|
|
2080
|
+
try {
|
|
2081
|
+
const decoded = nip19.decode(nsec);
|
|
2082
|
+
if (decoded.type === "nsec") {
|
|
2083
|
+
const pk = getPublicKey(decoded.data);
|
|
2084
|
+
return nip19.npubEncode(pk);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
catch { /* ignore */ }
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
profileCmd
|
|
2091
|
+
.command("list")
|
|
2092
|
+
.description("List all profiles (► marks active)")
|
|
2093
|
+
.action(async () => {
|
|
2094
|
+
const config = await loadConfig(program.opts().profile);
|
|
2095
|
+
for (const [name, profile] of Object.entries(config.profiles)) {
|
|
2096
|
+
const isActive = name === config.activeProfile;
|
|
2097
|
+
const marker = isActive ? "►" : " ";
|
|
2098
|
+
let npubStr = "(no key)";
|
|
2099
|
+
if (profile.nsec) {
|
|
2100
|
+
const npub = nsecToNpub(profile.nsec);
|
|
2101
|
+
if (npub)
|
|
2102
|
+
npubStr = npub.slice(0, 12) + "..." + npub.slice(-4);
|
|
2103
|
+
}
|
|
2104
|
+
const boardCount = profile.boards?.length ?? 0;
|
|
2105
|
+
console.log(` ${marker} ${name.padEnd(14)} ${npubStr.padEnd(22)} ${boardCount} board${boardCount !== 1 ? "s" : ""}`);
|
|
2106
|
+
}
|
|
2107
|
+
process.exit(0);
|
|
2108
|
+
});
|
|
2109
|
+
profileCmd
|
|
2110
|
+
.command("add <name>")
|
|
2111
|
+
.description("Add a new profile (runs mini onboarding for the new identity)")
|
|
2112
|
+
.action(async (name) => {
|
|
2113
|
+
const config = await loadConfig(program.opts().profile);
|
|
2114
|
+
if (config.profiles[name]) {
|
|
2115
|
+
console.error(chalk.red(`Profile already exists: "${name}"`));
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2118
|
+
console.log();
|
|
2119
|
+
console.log(chalk.bold(`Setting up profile: ${name}`));
|
|
2120
|
+
console.log();
|
|
2121
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2122
|
+
const ask = makeLineQueue(rl);
|
|
2123
|
+
// Key setup
|
|
2124
|
+
const hasKey = await ask("Do you have a Nostr private key (nsec)? [Y/n] ");
|
|
2125
|
+
let nsec;
|
|
2126
|
+
if (hasKey.trim().toLowerCase() !== "n") {
|
|
2127
|
+
while (true) {
|
|
2128
|
+
const input = (await ask("Paste your nsec: ")).trim();
|
|
2129
|
+
if (input.startsWith("nsec1")) {
|
|
2130
|
+
try {
|
|
2131
|
+
nip19.decode(input);
|
|
2132
|
+
nsec = input;
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
catch { /* invalid */ }
|
|
2136
|
+
}
|
|
2137
|
+
console.log("Invalid nsec. Try again or press Ctrl+C to abort.");
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
else {
|
|
2141
|
+
const sk = generateSecretKey();
|
|
2142
|
+
const pk = getPublicKey(sk);
|
|
2143
|
+
nsec = nip19.nsecEncode(sk);
|
|
2144
|
+
const npub = nip19.npubEncode(pk);
|
|
2145
|
+
console.log();
|
|
2146
|
+
console.log("✓ Generated new Nostr identity");
|
|
2147
|
+
console.log(` npub: ${npub}`);
|
|
2148
|
+
console.log(` nsec: ${nsec} ← KEEP THIS SECRET — it is your password`);
|
|
2149
|
+
console.log();
|
|
2150
|
+
console.log("Save this nsec somewhere safe. It cannot be recovered if lost.");
|
|
2151
|
+
const cont = await ask("Continue? [Y/n] ");
|
|
2152
|
+
if (cont.trim().toLowerCase() === "n") {
|
|
2153
|
+
rl.close();
|
|
2154
|
+
process.exit(0);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
// Relays setup
|
|
2158
|
+
console.log();
|
|
2159
|
+
let relays = [...DEFAULT_RELAYS];
|
|
2160
|
+
const useDefaults = await ask("Use default relays? [Y/n] ");
|
|
2161
|
+
if (useDefaults.trim().toLowerCase() === "n") {
|
|
2162
|
+
relays = [];
|
|
2163
|
+
while (true) {
|
|
2164
|
+
const relay = (await ask("Add relay URL (blank to finish): ")).trim();
|
|
2165
|
+
if (!relay)
|
|
2166
|
+
break;
|
|
2167
|
+
relays.push(relay);
|
|
2168
|
+
}
|
|
2169
|
+
if (relays.length === 0)
|
|
2170
|
+
relays = [...DEFAULT_RELAYS];
|
|
2171
|
+
}
|
|
2172
|
+
rl.close();
|
|
2173
|
+
const newProfile = {
|
|
2174
|
+
nsec,
|
|
2175
|
+
relays,
|
|
2176
|
+
boards: [],
|
|
2177
|
+
trustedNpubs: [],
|
|
2178
|
+
securityMode: "moderate",
|
|
2179
|
+
securityEnabled: true,
|
|
2180
|
+
defaultBoard: "Personal",
|
|
2181
|
+
taskReminders: {},
|
|
2182
|
+
};
|
|
2183
|
+
const newProfiles = { ...config.profiles, [name]: newProfile };
|
|
2184
|
+
await saveProfiles(config.activeProfile, newProfiles);
|
|
2185
|
+
console.log();
|
|
2186
|
+
console.log(chalk.green(`✓ Profile '${name}' created. Run: taskify profile use ${name}`));
|
|
2187
|
+
process.exit(0);
|
|
2188
|
+
});
|
|
2189
|
+
profileCmd
|
|
2190
|
+
.command("use <name>")
|
|
2191
|
+
.description("Switch the active profile")
|
|
2192
|
+
.action(async (name) => {
|
|
2193
|
+
const config = await loadConfig(program.opts().profile);
|
|
2194
|
+
if (!config.profiles[name]) {
|
|
2195
|
+
console.error(chalk.red(`Profile not found: "${name}". Available: ${Object.keys(config.profiles).join(", ")}`));
|
|
2196
|
+
process.exit(1);
|
|
2197
|
+
}
|
|
2198
|
+
await saveProfiles(name, config.profiles);
|
|
2199
|
+
console.log(chalk.green(`✓ Switched to profile: ${name}`));
|
|
2200
|
+
process.exit(0);
|
|
2201
|
+
});
|
|
2202
|
+
profileCmd
|
|
2203
|
+
.command("show [name]")
|
|
2204
|
+
.description("Show profile details (defaults to active profile)")
|
|
2205
|
+
.action(async (name) => {
|
|
2206
|
+
const config = await loadConfig(program.opts().profile);
|
|
2207
|
+
const profileName = name ?? config.activeProfile;
|
|
2208
|
+
const profile = config.profiles[profileName];
|
|
2209
|
+
if (!profile) {
|
|
2210
|
+
console.error(chalk.red(`Profile not found: "${profileName}". Available: ${Object.keys(config.profiles).join(", ")}`));
|
|
2211
|
+
process.exit(1);
|
|
2212
|
+
}
|
|
2213
|
+
const isActive = profileName === config.activeProfile;
|
|
2214
|
+
console.log(chalk.bold(`Profile: ${profileName}${isActive ? " ◄ active" : ""}`));
|
|
2215
|
+
let npubStr = "(no key)";
|
|
2216
|
+
if (profile.nsec) {
|
|
2217
|
+
const npub = nsecToNpub(profile.nsec);
|
|
2218
|
+
if (npub)
|
|
2219
|
+
npubStr = npub;
|
|
2220
|
+
}
|
|
2221
|
+
const maskedNsec = profile.nsec ? profile.nsec.slice(0, 8) + "..." : "(not set)";
|
|
2222
|
+
console.log(` nsec: ${maskedNsec}`);
|
|
2223
|
+
console.log(` npub: ${npubStr}`);
|
|
2224
|
+
console.log(` relays: ${(profile.relays ?? []).join(", ")}`);
|
|
2225
|
+
console.log(` boards: ${profile.boards?.length ?? 0}`);
|
|
2226
|
+
console.log(` trustedNpubs: ${profile.trustedNpubs?.length ?? 0}`);
|
|
2227
|
+
process.exit(0);
|
|
2228
|
+
});
|
|
2229
|
+
profileCmd
|
|
2230
|
+
.command("remove <name>")
|
|
2231
|
+
.description("Remove a profile")
|
|
2232
|
+
.option("--force", "Skip confirmation prompt")
|
|
2233
|
+
.action(async (name, opts) => {
|
|
2234
|
+
const config = await loadConfig(program.opts().profile);
|
|
2235
|
+
if (!config.profiles[name]) {
|
|
2236
|
+
console.error(chalk.red(`Profile not found: "${name}"`));
|
|
2237
|
+
process.exit(1);
|
|
2238
|
+
}
|
|
2239
|
+
if (name === config.activeProfile) {
|
|
2240
|
+
console.error(chalk.red(`Cannot remove active profile: "${name}". Switch first with: taskify profile use <other>`));
|
|
2241
|
+
process.exit(1);
|
|
2242
|
+
}
|
|
2243
|
+
if (Object.keys(config.profiles).length === 1) {
|
|
2244
|
+
console.error(chalk.red("Cannot remove the only profile."));
|
|
2245
|
+
process.exit(1);
|
|
2246
|
+
}
|
|
2247
|
+
if (!opts.force) {
|
|
2248
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2249
|
+
const confirmed = await new Promise((resolve) => {
|
|
2250
|
+
rl.question(`Remove profile '${name}'? [y/N] `, (ans) => {
|
|
2251
|
+
rl.close();
|
|
2252
|
+
resolve(ans.toLowerCase() === "y");
|
|
2253
|
+
});
|
|
2254
|
+
});
|
|
2255
|
+
if (!confirmed) {
|
|
2256
|
+
console.log("Aborted.");
|
|
2257
|
+
process.exit(0);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const { [name]: _removed, ...rest } = config.profiles;
|
|
2261
|
+
await saveProfiles(config.activeProfile, rest);
|
|
2262
|
+
console.log(chalk.green(`✓ Profile '${name}' removed.`));
|
|
2263
|
+
process.exit(0);
|
|
2264
|
+
});
|
|
2265
|
+
profileCmd
|
|
2266
|
+
.command("rename <old> <new>")
|
|
2267
|
+
.description("Rename a profile")
|
|
2268
|
+
.action(async (oldName, newName) => {
|
|
2269
|
+
const config = await loadConfig(program.opts().profile);
|
|
2270
|
+
if (!config.profiles[oldName]) {
|
|
2271
|
+
console.error(chalk.red(`Profile not found: "${oldName}"`));
|
|
2272
|
+
process.exit(1);
|
|
2273
|
+
}
|
|
2274
|
+
if (config.profiles[newName]) {
|
|
2275
|
+
console.error(chalk.red(`Profile already exists: "${newName}"`));
|
|
2276
|
+
process.exit(1);
|
|
2277
|
+
}
|
|
2278
|
+
const { [oldName]: profileData, ...rest } = config.profiles;
|
|
2279
|
+
const newProfiles = { ...rest, [newName]: profileData };
|
|
2280
|
+
const newActive = config.activeProfile === oldName ? newName : config.activeProfile;
|
|
2281
|
+
await saveProfiles(newActive, newProfiles);
|
|
2282
|
+
console.log(chalk.green(`✓ Renamed profile '${oldName}' → '${newName}'`));
|
|
2283
|
+
process.exit(0);
|
|
2284
|
+
});
|
|
2049
2285
|
// ---- setup ----
|
|
2050
2286
|
program
|
|
2051
2287
|
.command("setup")
|
|
2052
|
-
.description("Run the first-run onboarding wizard (re-configure
|
|
2053
|
-
.
|
|
2054
|
-
|
|
2288
|
+
.description("Run the first-run onboarding wizard (re-configure a profile)")
|
|
2289
|
+
.option("--profile <name>", "Profile to configure (defaults to active profile)")
|
|
2290
|
+
.action(async (opts) => {
|
|
2291
|
+
// --profile on setup subcommand takes precedence over global --profile
|
|
2292
|
+
const targetProfile = opts.profile ?? program.opts().profile;
|
|
2293
|
+
const existing = await loadConfig(targetProfile);
|
|
2055
2294
|
if (existing.nsec) {
|
|
2056
2295
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2057
2296
|
const ans = await new Promise((resolve) => {
|
|
2058
|
-
rl.question("
|
|
2297
|
+
rl.question(`⚠ Profile "${existing.activeProfile}" already has a private key. This will replace it.\nContinue? [Y/n] `, resolve);
|
|
2059
2298
|
});
|
|
2060
2299
|
rl.close();
|
|
2061
2300
|
if (ans.trim().toLowerCase() === "n") {
|
|
2062
2301
|
process.exit(0);
|
|
2063
2302
|
}
|
|
2064
2303
|
}
|
|
2065
|
-
await runOnboarding();
|
|
2304
|
+
await runOnboarding(targetProfile ?? existing.activeProfile);
|
|
2066
2305
|
});
|
|
2067
2306
|
// ---- auto-onboarding trigger + parse ----
|
|
2068
|
-
const cfg = await loadConfig();
|
|
2069
|
-
if
|
|
2307
|
+
const cfg = await loadConfig(program.opts().profile);
|
|
2308
|
+
// Trigger onboarding if no profiles have an nsec and no command was given
|
|
2309
|
+
const hasAnyNsec = Object.values(cfg.profiles).some((p) => p.nsec);
|
|
2310
|
+
if (!hasAnyNsec && process.argv.length <= 2) {
|
|
2070
2311
|
await runOnboarding();
|
|
2071
2312
|
}
|
|
2072
2313
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|