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 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,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 or add a new key)")
2053
- .action(async () => {
2054
- const existing = await loadConfig();
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(" You already have a private key configured. This will replace it.\nContinue? [Y/n] ", resolve);
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 (!cfg.nsec && process.argv.length <= 2) {
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 {
@@ -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.0",
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",