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 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 DEFAULT_CONFIG = {
8
- relays: [
9
- "wss://relay.damus.io",
10
- "wss://nos.lol",
11
- "wss://relay.snort.social",
12
- "wss://relay.primal.net",
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
- export async function loadConfig() {
22
- let cfg;
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
- cfg = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
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
- cfg = { ...DEFAULT_CONFIG };
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
- cfg.nsec = process.env.TASKIFY_NSEC;
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 cfg;
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
- await writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
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 { nip19 } from "nostr-tools";
7
- import { loadConfig, saveConfig } from "./config.js";
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("0.1.0")
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 or add a new key)")
2053
- .action(async () => {
2054
- const existing = await loadConfig();
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(" You already have a private key configured. This will replace it.\nContinue? [Y/n] ", resolve);
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 (!cfg.nsec && process.argv.length <= 2) {
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 {
@@ -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, loadConfig } from "./config.js";
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
- // Load fresh config to ensure we have the latest board entry
432
- const cfg = await loadConfig();
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(cfg);
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
- const cfg = await loadConfig();
806
- if (!cfg.taskReminders)
807
- cfg.taskReminders = {};
808
- cfg.taskReminders[taskId] = presets;
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: cfg.securityEnabled,
819
- mode: cfg.securityMode,
820
- trustedNpubs: cfg.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
- const cfg = await loadConfig();
826
- cfg.securityEnabled = secCfg.enabled;
827
- cfg.securityMode = secCfg.mode;
828
- cfg.trustedNpubs = secCfg.trustedNpubs;
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
- cfg.boards.push(newEntry);
884
- await saveConfig(cfg);
878
+ config.boards.push(newEntry);
879
+ await saveConfig(config);
885
880
  return { boardId };
886
881
  },
887
882
  };
@@ -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, saveConfig } from "./config.js";
5
- function ask(rl, question) {
6
- return new Promise((resolve) => rl.question(question, resolve));
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
- export async function runOnboarding() {
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 cfg = await loadConfig();
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(rl, "Do you have a Nostr private key (nsec)? [Y/n] ");
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
- nsec = (await ask(rl, "Paste your nsec: ")).trim();
28
- if (nsec.startsWith("nsec1")) {
60
+ const input = (await ask("Paste your nsec: ")).trim();
61
+ if (input.startsWith("nsec1")) {
29
62
  try {
30
- nip19.decode(nsec);
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
- const nsec = nip19.nsecEncode(sk);
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(rl, "Continue? [Y/n] ");
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(rl, "Do you want to join an existing board? [y/N] ");
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(rl, "Board ID (Nostr event id): ")).trim();
98
+ const boardId = (await ask("Board ID (Nostr event id): ")).trim();
66
99
  if (boardId) {
67
- cfg.defaultBoard = boardId;
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(rl, "Configure relays? Default relays will be used if skipped. [y/N] ");
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 relays = [];
108
+ const customRelays = [];
76
109
  while (true) {
77
- const relay = (await ask(rl, "Add relay URL (blank to finish): ")).trim();
110
+ const relay = (await ask("Add relay URL (blank to finish): ")).trim();
78
111
  if (!relay)
79
112
  break;
80
- relays.push(relay);
113
+ customRelays.push(relay);
81
114
  }
82
- if (relays.length > 0) {
83
- cfg.relays = relays;
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
- await saveConfig(cfg);
88
- // Step 4 Done
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("✓ Setup complete! Run `taskify boards` to see your boards.");
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.0",
3
+ "version": "0.2.1",
4
4
  "description": "Nostr-powered task management CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "taskify": "./dist/index.js"
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",