@neuralconfig/nrepo 0.0.2 → 0.0.4

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI for [NeuralRepo](https://neuralrepo.com) — AI-native idea capture and management.
4
4
 
5
- Capture ideas, search semantically, organize with tags and statuses, link related ideas, and pull context for development — all from the terminal. Commands mirror git vocabulary (`push`, `log`, `diff`, `branch`, `merge`, `tag`, `stash`) for familiarity to both humans and LLMs. Designed to compose with unix pipes and tools like `jq`.
5
+ Capture ideas, search semantically, organize with tags and statuses, link related ideas, and pull context for development — all from the terminal. Commands mirror git vocabulary (`push`, `log`, `diff`, `branch`, `merge`, `tag`, `rm`) for familiarity to both humans and LLMs. Designed to compose with unix pipes and tools like `jq`.
6
6
 
7
7
  ## Install
8
8
 
@@ -97,8 +97,8 @@ Always search before creating to avoid duplicates — the server runs semantic d
97
97
  # Full capture with body and tags
98
98
  nrepo push "Add rate limiting to API" --body "Sliding window algorithm, store in KV" --tag backend --tag infrastructure
99
99
 
100
- # Quick capture (title only)
101
- nrepo stash "Look into edge caching for static assets"
100
+ # Quick capture (title only — body and tags are optional)
101
+ nrepo push "Look into edge caching for static assets"
102
102
  ```
103
103
 
104
104
  **push options:** `--body <text>`, `--tag <tag>` (repeatable), `--status <status>`
@@ -229,12 +229,30 @@ nrepo pull 42 --to ./idea-context
229
229
  nrepo status # Idea counts by status, recent captures, pending duplicates
230
230
  ```
231
231
 
232
+ ### Archive ideas
233
+
234
+ ```bash
235
+ nrepo rm 42 # Archive (soft-delete) an idea
236
+ nrepo rm 42 --force # Skip confirmation
237
+ ```
238
+
239
+ ### Manage duplicates
240
+
241
+ The server automatically detects semantic duplicates. Review and resolve them:
242
+
243
+ ```bash
244
+ nrepo duplicate # List pending duplicate detections
245
+ nrepo duplicate list # Same as above
246
+ nrepo duplicate dismiss 7 # Dismiss a false positive
247
+ nrepo duplicate merge 7 # Merge duplicate into its primary idea
248
+ ```
249
+
232
250
  ### API key management
233
251
 
234
252
  ```bash
235
- nrepo keys list # List all API keys
236
- nrepo keys create "CI bot" # Generate a new key (shown once)
237
- nrepo keys revoke 7 # Revoke a key by ID
253
+ nrepo key list # List all API keys
254
+ nrepo key create "CI bot" # Generate a new key (shown once)
255
+ nrepo key revoke 7 # Revoke a key by ID
238
256
  ```
239
257
 
240
258
  ## JSON output and unix composition
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk18 from "chalk";
5
+ import chalk21 from "chalk";
6
6
 
7
7
  // src/config.ts
8
8
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -116,6 +116,9 @@ var createRelation = (c, sourceId, targetId, relationType = "related", note, for
116
116
  });
117
117
  };
118
118
  var deleteRelation = (c, relationId) => request(c, "DELETE", `/map/relations/${relationId}`);
119
+ var deleteIdea = (c, id) => request(c, "DELETE", `/ideas/${id}`);
120
+ var dismissDuplicate = (c, dupId) => request(c, "POST", `/ideas/duplicates/${dupId}/dismiss`);
121
+ var mergeDuplicate = (c, dupId) => request(c, "POST", `/ideas/duplicates/${dupId}/merge`);
119
122
  var mergeIdeas = (c, keepId, absorbId) => request(c, "POST", `/ideas/${keepId}/merge`, { absorb_id: absorbId });
120
123
 
121
124
  // src/commands/login.ts
@@ -392,9 +395,9 @@ function formatIdeaDetail(idea) {
392
395
  }
393
396
  return lines.join("\n");
394
397
  }
395
- function formatDuplicate(dup) {
396
- const score = chalk3.yellow(`${(dup.similarity_score * 100).toFixed(0)}%`);
397
- return ` ${chalk3.dim(`#${dup.id}`)} ${dup.idea_title} ${chalk3.dim("\u2248")} ${dup.duplicate_title} ${score}`;
398
+ function formatDuplicate(dup2) {
399
+ const score = chalk3.yellow(`${(dup2.similarity_score * 100).toFixed(0)}%`);
400
+ return ` ${chalk3.dim(`#${dup2.id}`)} ${dup2.idea_title} ${chalk3.dim("\u2248")} ${dup2.duplicate_title} ${score}`;
398
401
  }
399
402
  function formatDate(iso) {
400
403
  const normalized = iso.includes("T") || iso.includes("Z") ? iso : iso.replace(" ", "T") + "Z";
@@ -437,9 +440,6 @@ async function pushCommand(title, opts) {
437
440
  console.log(chalk4.dim("\n Processing: embeddings, dedup, and auto-tagging queued"));
438
441
  }
439
442
  }
440
- async function stashCommand(title, opts) {
441
- await pushCommand(title, { json: opts.json });
442
- }
443
443
 
444
444
  // src/commands/search.ts
445
445
  import chalk5 from "chalk";
@@ -541,8 +541,8 @@ async function statusCommand(opts) {
541
541
  const pendingDups = dupsData.duplicates.filter((d) => d.status === "pending");
542
542
  if (pendingDups.length > 0) {
543
543
  console.log(chalk7.bold(`Pending duplicates (${pendingDups.length})`));
544
- for (const dup of pendingDups.slice(0, 5)) {
545
- console.log(formatDuplicate(dup));
544
+ for (const dup2 of pendingDups.slice(0, 5)) {
545
+ console.log(formatDuplicate(dup2));
546
546
  }
547
547
  }
548
548
  }
@@ -966,7 +966,7 @@ async function editCommand(id, opts) {
966
966
  if (opts.body) console.log(` Body updated (${updated.body?.length ?? 0} chars)`);
967
967
  }
968
968
 
969
- // src/commands/keys.ts
969
+ // src/commands/key.ts
970
970
  import chalk14 from "chalk";
971
971
  import ora13 from "ora";
972
972
  async function keysListCommand(opts) {
@@ -979,7 +979,7 @@ async function keysListCommand(opts) {
979
979
  return;
980
980
  }
981
981
  if (api_keys.length === 0) {
982
- console.log(chalk14.dim("No API keys found. Create one with `nrepo keys create <label>`."));
982
+ console.log(chalk14.dim("No API keys found. Create one with `nrepo key create <label>`."));
983
983
  return;
984
984
  }
985
985
  console.log(chalk14.bold(`${api_keys.length} API key${api_keys.length === 1 ? "" : "s"}
@@ -1018,9 +1018,97 @@ async function keysRevokeCommand(keyId, opts) {
1018
1018
  console.log(chalk14.green("\u2713") + ` API key ${chalk14.dim(keyId)} revoked.`);
1019
1019
  }
1020
1020
 
1021
- // src/commands/link.ts
1021
+ // src/commands/rm.ts
1022
1022
  import chalk15 from "chalk";
1023
1023
  import ora14 from "ora";
1024
+ async function rmCommand(id, opts) {
1025
+ const config = await getAuthenticatedConfig();
1026
+ const ideaId = parseInt(id, 10);
1027
+ if (isNaN(ideaId)) {
1028
+ console.error("Invalid idea ID");
1029
+ process.exit(1);
1030
+ }
1031
+ const spinner = opts.json ? null : ora14("Loading idea...").start();
1032
+ const idea = await getIdea(config, ideaId);
1033
+ spinner?.stop();
1034
+ if (!opts.json && !opts.force) {
1035
+ console.log(chalk15.bold("Archive preview:"));
1036
+ console.log(` #${ideaId} "${idea.title}" [${idea.status}]`);
1037
+ console.log("");
1038
+ console.log(chalk15.dim(" The idea will be archived (soft-deleted)."));
1039
+ console.log("");
1040
+ }
1041
+ const archiveSpinner = opts.json ? null : ora14("Archiving idea...").start();
1042
+ await deleteIdea(config, ideaId);
1043
+ archiveSpinner?.stop();
1044
+ if (opts.json) {
1045
+ console.log(JSON.stringify({ success: true, archived: ideaId }));
1046
+ return;
1047
+ }
1048
+ console.log(chalk15.green("\u2713") + ` Archived #${ideaId} "${idea.title}"`);
1049
+ }
1050
+
1051
+ // src/commands/duplicate.ts
1052
+ import chalk16 from "chalk";
1053
+ import ora15 from "ora";
1054
+ async function duplicateListCommand(opts) {
1055
+ const config = await getAuthenticatedConfig();
1056
+ const spinner = opts.json ? null : ora15("Loading duplicates...").start();
1057
+ const { duplicates } = await listDuplicates(config);
1058
+ spinner?.stop();
1059
+ if (opts.json) {
1060
+ console.log(JSON.stringify({ duplicates }, null, 2));
1061
+ return;
1062
+ }
1063
+ const pending = duplicates.filter((d) => d.status === "pending");
1064
+ if (pending.length === 0) {
1065
+ console.log(chalk16.dim("No pending duplicates."));
1066
+ return;
1067
+ }
1068
+ console.log(chalk16.bold(`${pending.length} pending duplicate${pending.length === 1 ? "" : "s"}
1069
+ `));
1070
+ for (const dup2 of pending) {
1071
+ console.log(formatDuplicate(dup2));
1072
+ }
1073
+ console.log("");
1074
+ console.log(chalk16.dim(" Use `nrepo duplicate dismiss <id>` or `nrepo duplicate merge <id>` to resolve."));
1075
+ }
1076
+ async function duplicateDismissCommand(id, opts) {
1077
+ const config = await getAuthenticatedConfig();
1078
+ const dupId = parseInt(id, 10);
1079
+ if (isNaN(dupId)) {
1080
+ console.error("Invalid duplicate ID");
1081
+ process.exit(1);
1082
+ }
1083
+ const spinner = opts.json ? null : ora15("Dismissing duplicate...").start();
1084
+ await dismissDuplicate(config, dupId);
1085
+ spinner?.stop();
1086
+ if (opts.json) {
1087
+ console.log(JSON.stringify({ success: true, dismissed: dupId }));
1088
+ return;
1089
+ }
1090
+ console.log(chalk16.green("\u2713") + ` Dismissed duplicate #${dupId}`);
1091
+ }
1092
+ async function duplicateMergeCommand(id, opts) {
1093
+ const config = await getAuthenticatedConfig();
1094
+ const dupId = parseInt(id, 10);
1095
+ if (isNaN(dupId)) {
1096
+ console.error("Invalid duplicate ID");
1097
+ process.exit(1);
1098
+ }
1099
+ const spinner = opts.json ? null : ora15("Merging duplicate...").start();
1100
+ await mergeDuplicate(config, dupId);
1101
+ spinner?.stop();
1102
+ if (opts.json) {
1103
+ console.log(JSON.stringify({ success: true, merged: dupId }));
1104
+ return;
1105
+ }
1106
+ console.log(chalk16.green("\u2713") + ` Merged duplicate #${dupId} into primary idea`);
1107
+ }
1108
+
1109
+ // src/commands/link.ts
1110
+ import chalk17 from "chalk";
1111
+ import ora16 from "ora";
1024
1112
  var VALID_TYPES = RELATION_TYPES.filter((t) => t !== "duplicate");
1025
1113
  async function linkCommand(sourceId, targetId, opts) {
1026
1114
  const config = await getAuthenticatedConfig();
@@ -1035,7 +1123,7 @@ async function linkCommand(sourceId, targetId, opts) {
1035
1123
  console.error(`Invalid type "${relationType}". Must be one of: ${VALID_TYPES.join(", ")}`);
1036
1124
  process.exit(1);
1037
1125
  }
1038
- const spinner = opts.json ? null : ora14("Creating link...").start();
1126
+ const spinner = opts.json ? null : ora16("Creating link...").start();
1039
1127
  try {
1040
1128
  const result = await createRelation(config, src, tgt, relationType, opts.note, opts.force);
1041
1129
  spinner?.stop();
@@ -1043,9 +1131,9 @@ async function linkCommand(sourceId, targetId, opts) {
1043
1131
  console.log(JSON.stringify(result.relation, null, 2));
1044
1132
  return;
1045
1133
  }
1046
- console.log(chalk15.green("\u2713") + ` Linked #${src} \u2192 #${tgt} (${relationType})`);
1134
+ console.log(chalk17.green("\u2713") + ` Linked #${src} \u2192 #${tgt} (${relationType})`);
1047
1135
  if (opts.note) {
1048
- console.log(chalk15.dim(` Note: ${opts.note}`));
1136
+ console.log(chalk17.dim(` Note: ${opts.note}`));
1049
1137
  }
1050
1138
  } catch (err) {
1051
1139
  spinner?.stop();
@@ -1053,9 +1141,9 @@ async function linkCommand(sourceId, targetId, opts) {
1053
1141
  if (opts.json) {
1054
1142
  console.error(JSON.stringify({ error: err.message, code: "cycle_detected" }));
1055
1143
  } else {
1056
- console.error(chalk15.red(err.message));
1144
+ console.error(chalk17.red(err.message));
1057
1145
  if (!opts.force && (relationType === "supersedes" || relationType === "parent")) {
1058
- console.error(chalk15.dim(" Use --force to bypass this check."));
1146
+ console.error(chalk17.dim(" Use --force to bypass this check."));
1059
1147
  }
1060
1148
  }
1061
1149
  process.exit(1);
@@ -1071,7 +1159,7 @@ async function unlinkCommand(sourceId, targetId, opts) {
1071
1159
  console.error("Invalid idea IDs");
1072
1160
  process.exit(1);
1073
1161
  }
1074
- const spinner = opts.json ? null : ora14("Removing link...").start();
1162
+ const spinner = opts.json ? null : ora16("Removing link...").start();
1075
1163
  const relations = await getIdeaRelations(config, src);
1076
1164
  const match = relations.outgoing.find((r) => r.idea_id === tgt) ?? relations.incoming.find((r) => r.idea_id === tgt);
1077
1165
  if (!match) {
@@ -1079,7 +1167,7 @@ async function unlinkCommand(sourceId, targetId, opts) {
1079
1167
  if (opts.json) {
1080
1168
  console.error(JSON.stringify({ error: "No link found between these ideas" }));
1081
1169
  } else {
1082
- console.error(`No link found between #${src} and #${tgt}. Run ${chalk15.cyan(`nrepo links ${src}`)} to see existing links.`);
1170
+ console.error(`No link found between #${src} and #${tgt}. Run ${chalk17.cyan(`nrepo links ${src}`)} to see existing links.`);
1083
1171
  }
1084
1172
  process.exit(1);
1085
1173
  }
@@ -1089,7 +1177,7 @@ async function unlinkCommand(sourceId, targetId, opts) {
1089
1177
  console.log(JSON.stringify({ success: true }));
1090
1178
  return;
1091
1179
  }
1092
- console.log(chalk15.green("\u2713") + ` Unlinked #${src} \u2194 #${tgt}`);
1180
+ console.log(chalk17.green("\u2713") + ` Unlinked #${src} \u2194 #${tgt}`);
1093
1181
  }
1094
1182
  async function linksCommand(id, opts) {
1095
1183
  const config = await getAuthenticatedConfig();
@@ -1098,7 +1186,7 @@ async function linksCommand(id, opts) {
1098
1186
  console.error("Invalid idea ID");
1099
1187
  process.exit(1);
1100
1188
  }
1101
- const spinner = opts.json ? null : ora14("Loading links...").start();
1189
+ const spinner = opts.json ? null : ora16("Loading links...").start();
1102
1190
  const [idea, relations] = await Promise.all([
1103
1191
  getIdea(config, ideaId),
1104
1192
  getIdeaRelations(config, ideaId)
@@ -1113,9 +1201,9 @@ async function linksCommand(id, opts) {
1113
1201
  console.log(JSON.stringify({ outgoing, incoming }, null, 2));
1114
1202
  return;
1115
1203
  }
1116
- console.log(chalk15.bold(`Links for #${ideaId} "${idea.title}":`));
1204
+ console.log(chalk17.bold(`Links for #${ideaId} "${idea.title}":`));
1117
1205
  if (outgoing.length === 0 && incoming.length === 0) {
1118
- console.log(chalk15.dim(" No links"));
1206
+ console.log(chalk17.dim(" No links"));
1119
1207
  return;
1120
1208
  }
1121
1209
  const DISPLAY = {
@@ -1141,10 +1229,10 @@ async function linksCommand(id, opts) {
1141
1229
  for (const [type, items] of outByType) {
1142
1230
  const d = DISPLAY[type] ?? { out: type, arrow: "\u2192" };
1143
1231
  console.log("");
1144
- console.log(` ${chalk15.bold(d.out)} ${d.arrow}`);
1232
+ console.log(` ${chalk17.bold(d.out)} ${d.arrow}`);
1145
1233
  for (const r of items) {
1146
- const status = chalk15.dim(`[${r.idea_status}]`);
1147
- const note = r.note ? chalk15.dim(` \u2014 ${r.note}`) : "";
1234
+ const status = chalk17.dim(`[${r.idea_status}]`);
1235
+ const note = r.note ? chalk17.dim(` \u2014 ${r.note}`) : "";
1148
1236
  console.log(` #${r.idea_id} ${r.idea_title} ${status}${note}`);
1149
1237
  }
1150
1238
  }
@@ -1152,10 +1240,10 @@ async function linksCommand(id, opts) {
1152
1240
  if ((type === "related" || type === "duplicate") && outByType.has(type)) continue;
1153
1241
  const d = DISPLAY[type] ?? { in: type, arrow: "\u2190" };
1154
1242
  console.log("");
1155
- console.log(` ${chalk15.bold(d.in)} \u2190`);
1243
+ console.log(` ${chalk17.bold(d.in)} \u2190`);
1156
1244
  for (const r of items) {
1157
- const status = chalk15.dim(`[${r.idea_status}]`);
1158
- const note = r.note ? chalk15.dim(` \u2014 ${r.note}`) : "";
1245
+ const status = chalk17.dim(`[${r.idea_status}]`);
1246
+ const note = r.note ? chalk17.dim(` \u2014 ${r.note}`) : "";
1159
1247
  console.log(` #${r.idea_id} ${r.idea_title} ${status}${note}`);
1160
1248
  }
1161
1249
  }
@@ -1163,8 +1251,8 @@ async function linksCommand(id, opts) {
1163
1251
  }
1164
1252
 
1165
1253
  // src/commands/merge.ts
1166
- import chalk16 from "chalk";
1167
- import ora15 from "ora";
1254
+ import chalk18 from "chalk";
1255
+ import ora17 from "ora";
1168
1256
  async function mergeCommand(keepId, absorbId, opts) {
1169
1257
  const config = await getAuthenticatedConfig();
1170
1258
  const keep = parseInt(keepId, 10);
@@ -1177,37 +1265,37 @@ async function mergeCommand(keepId, absorbId, opts) {
1177
1265
  console.error("Cannot merge an idea with itself");
1178
1266
  process.exit(1);
1179
1267
  }
1180
- const spinner = opts.json ? null : ora15("Loading ideas...").start();
1268
+ const spinner = opts.json ? null : ora17("Loading ideas...").start();
1181
1269
  const [keepIdea, absorbIdea] = await Promise.all([
1182
1270
  getIdea(config, keep),
1183
1271
  getIdea(config, absorb)
1184
1272
  ]);
1185
1273
  spinner?.stop();
1186
1274
  if (!opts.json && !opts.force) {
1187
- console.log(chalk16.bold("Merge preview:"));
1275
+ console.log(chalk18.bold("Merge preview:"));
1188
1276
  console.log(` Keep: #${keep} "${keepIdea.title}" [${keepIdea.status}]`);
1189
1277
  console.log(` Absorb: #${absorb} "${absorbIdea.title}" [${absorbIdea.status}]`);
1190
1278
  console.log("");
1191
- console.log(chalk16.dim(" The absorbed idea will be shelved and archived."));
1192
- console.log(chalk16.dim(" Bodies will be concatenated, tags merged."));
1279
+ console.log(chalk18.dim(" The absorbed idea will be shelved and archived."));
1280
+ console.log(chalk18.dim(" Bodies will be concatenated, tags merged."));
1193
1281
  console.log("");
1194
1282
  }
1195
- const mergeSpinner = opts.json ? null : ora15("Merging ideas...").start();
1283
+ const mergeSpinner = opts.json ? null : ora17("Merging ideas...").start();
1196
1284
  const result = await mergeIdeas(config, keep, absorb);
1197
1285
  mergeSpinner?.stop();
1198
1286
  if (opts.json) {
1199
1287
  console.log(JSON.stringify(result, null, 2));
1200
1288
  return;
1201
1289
  }
1202
- console.log(chalk16.green("\u2713") + ` Merged #${absorb} into #${keep}`);
1290
+ console.log(chalk18.green("\u2713") + ` Merged #${absorb} into #${keep}`);
1203
1291
  console.log(` Title: "${result.title}"`);
1204
1292
  console.log(` Tags: ${result.tags?.length ? result.tags.join(", ") : "none"}`);
1205
1293
  console.log(` #${absorb} "${absorbIdea.title}" \u2192 shelved (superseded by #${keep})`);
1206
1294
  }
1207
1295
 
1208
1296
  // src/commands/graph.ts
1209
- import chalk17 from "chalk";
1210
- import ora16 from "ora";
1297
+ import chalk19 from "chalk";
1298
+ import ora18 from "ora";
1211
1299
  async function graphCommand(id, opts) {
1212
1300
  const config = await getAuthenticatedConfig();
1213
1301
  const startId = parseInt(id, 10);
@@ -1217,7 +1305,7 @@ async function graphCommand(id, opts) {
1217
1305
  }
1218
1306
  const maxDepth = Math.min(parseInt(opts.depth ?? "1", 10), 5);
1219
1307
  const typeFilter = opts.type?.split(",");
1220
- const spinner = opts.json ? null : ora16("Traversing graph...").start();
1308
+ const spinner = opts.json ? null : ora18("Traversing graph...").start();
1221
1309
  const visited = /* @__PURE__ */ new Map();
1222
1310
  const edges = [];
1223
1311
  const children = /* @__PURE__ */ new Map();
@@ -1267,23 +1355,23 @@ async function graphCommand(id, opts) {
1267
1355
  return;
1268
1356
  }
1269
1357
  const statusStyle3 = {
1270
- captured: chalk17.gray,
1271
- exploring: chalk17.cyan,
1272
- building: chalk17.yellow,
1273
- shipped: chalk17.green,
1274
- shelved: chalk17.dim
1358
+ captured: chalk19.gray,
1359
+ exploring: chalk19.cyan,
1360
+ building: chalk19.yellow,
1361
+ shipped: chalk19.green,
1362
+ shelved: chalk19.dim
1275
1363
  };
1276
1364
  const typeColor = {
1277
- blocks: chalk17.red,
1278
- inspires: chalk17.cyan,
1279
- supersedes: chalk17.dim,
1280
- parent: chalk17.white,
1281
- related: chalk17.magenta,
1282
- duplicate: chalk17.yellow
1365
+ blocks: chalk19.red,
1366
+ inspires: chalk19.cyan,
1367
+ supersedes: chalk19.dim,
1368
+ parent: chalk19.white,
1369
+ related: chalk19.magenta,
1370
+ duplicate: chalk19.yellow
1283
1371
  };
1284
1372
  function renderTree(nodeId, prefix, isLast, isRoot) {
1285
1373
  const node = visited.get(nodeId);
1286
- const style = statusStyle3[node.status] ?? chalk17.white;
1374
+ const style = statusStyle3[node.status] ?? chalk19.white;
1287
1375
  const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1288
1376
  const childPrefix = isRoot ? "" : isLast ? " " : "\u2502 ";
1289
1377
  const nodeChildren = children.get(nodeId) ?? [];
@@ -1295,9 +1383,9 @@ async function graphCommand(id, opts) {
1295
1383
  }
1296
1384
  for (const [i, child] of nodeChildren.entries()) {
1297
1385
  const last = i === nodeChildren.length - 1;
1298
- const tc = typeColor[child.type] ?? chalk17.white;
1386
+ const tc = typeColor[child.type] ?? chalk19.white;
1299
1387
  const childNode = visited.get(child.childId);
1300
- const childStyle = statusStyle3[childNode.status] ?? chalk17.white;
1388
+ const childStyle = statusStyle3[childNode.status] ?? chalk19.white;
1301
1389
  const childConnector = last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1302
1390
  const nextPrefix = prefix + childPrefix + (last ? " " : "\u2502 ");
1303
1391
  console.log(
@@ -1308,8 +1396,8 @@ async function graphCommand(id, opts) {
1308
1396
  const gcLast = j === grandchildren.length - 1;
1309
1397
  const gcNode = visited.get(gc.childId);
1310
1398
  if (!gcNode) continue;
1311
- const gcStyle = statusStyle3[gcNode.status] ?? chalk17.white;
1312
- const gcTc = typeColor[gc.type] ?? chalk17.white;
1399
+ const gcStyle = statusStyle3[gcNode.status] ?? chalk19.white;
1400
+ const gcTc = typeColor[gc.type] ?? chalk19.white;
1313
1401
  const gcConnector = gcLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1314
1402
  console.log(
1315
1403
  `${nextPrefix}${gcConnector}${gcTc(gc.type)} \u2192 #${gc.childId} ${gc.title} ${gcStyle(`[${gcNode.status}]`)}`
@@ -1319,17 +1407,112 @@ async function graphCommand(id, opts) {
1319
1407
  }
1320
1408
  if (visited.size === 1) {
1321
1409
  const root = visited.get(startId);
1322
- const style = statusStyle3[root.status] ?? chalk17.white;
1410
+ const style = statusStyle3[root.status] ?? chalk19.white;
1323
1411
  console.log(`#${root.id} ${root.title} ${style(`[${root.status}]`)}`);
1324
- console.log(chalk17.dim(" No connections found"));
1412
+ console.log(chalk19.dim(" No connections found"));
1325
1413
  } else {
1326
1414
  renderTree(startId, "", true, true);
1327
1415
  }
1328
1416
  }
1329
1417
 
1418
+ // src/update-check.ts
1419
+ import { readFileSync, existsSync as existsSync2 } from "fs";
1420
+ import { writeFile as writeFile3, mkdir as mkdir3, copyFile } from "fs/promises";
1421
+ import { homedir as homedir2 } from "os";
1422
+ import { join as join3, dirname } from "path";
1423
+ import { fileURLToPath } from "url";
1424
+ import chalk20 from "chalk";
1425
+ var CHECK_FILE = join3(CONFIG_DIR, "update-check.json");
1426
+ var WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
1427
+ var PACKAGE_NAME = "@neuralconfig/nrepo";
1428
+ function checkForUpdates(currentVersion) {
1429
+ if (process.env["NREPO_NO_UPDATE_CHECK"] === "1") return;
1430
+ try {
1431
+ const cached = readCachedCheck();
1432
+ if (cached?.latest_version && isNewer(cached.latest_version, currentVersion)) {
1433
+ printUpdateNotice(currentVersion, cached.latest_version);
1434
+ }
1435
+ if (!cached || isStale(cached.last_checked)) {
1436
+ fetchAndCache(currentVersion);
1437
+ }
1438
+ } catch {
1439
+ }
1440
+ }
1441
+ function printUpdateNotice(current, latest) {
1442
+ console.error(
1443
+ chalk20.dim(` nrepo ${latest} available (current: ${current}). Run `) + chalk20.dim.bold("npm i -g @neuralconfig/nrepo") + chalk20.dim(" to update.")
1444
+ );
1445
+ console.error("");
1446
+ }
1447
+ function readCachedCheck() {
1448
+ if (!existsSync2(CHECK_FILE)) return null;
1449
+ try {
1450
+ const raw = readFileSync(CHECK_FILE, "utf-8");
1451
+ return JSON.parse(raw);
1452
+ } catch {
1453
+ return null;
1454
+ }
1455
+ }
1456
+ function isStale(lastChecked) {
1457
+ return Date.now() - new Date(lastChecked).getTime() > WEEK_MS;
1458
+ }
1459
+ function isNewer(latest, current) {
1460
+ const l = latest.split(".").map(Number);
1461
+ const c = current.split(".").map(Number);
1462
+ for (let i = 0; i < 3; i++) {
1463
+ if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
1464
+ if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
1465
+ }
1466
+ return false;
1467
+ }
1468
+ function fetchAndCache(currentVersion) {
1469
+ (async () => {
1470
+ const controller = new AbortController();
1471
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1472
+ try {
1473
+ const res = await fetch(
1474
+ `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
1475
+ { signal: controller.signal }
1476
+ );
1477
+ clearTimeout(timeout);
1478
+ if (!res.ok) return;
1479
+ const data = await res.json();
1480
+ const latest = data.version;
1481
+ if (!existsSync2(CONFIG_DIR)) {
1482
+ await mkdir3(CONFIG_DIR, { recursive: true });
1483
+ }
1484
+ const check = {
1485
+ last_checked: (/* @__PURE__ */ new Date()).toISOString(),
1486
+ latest_version: latest
1487
+ };
1488
+ await writeFile3(CHECK_FILE, JSON.stringify(check, null, 2) + "\n", "utf-8");
1489
+ if (isNewer(latest, currentVersion)) {
1490
+ await updateSkillFile();
1491
+ }
1492
+ } catch {
1493
+ }
1494
+ })();
1495
+ }
1496
+ async function updateSkillFile() {
1497
+ try {
1498
+ const claudeDir = join3(homedir2(), ".claude");
1499
+ if (!existsSync2(claudeDir)) return;
1500
+ const skillDir = join3(claudeDir, "skills", "neuralrepo");
1501
+ if (!existsSync2(skillDir)) {
1502
+ await mkdir3(skillDir, { recursive: true });
1503
+ }
1504
+ const src = join3(dirname(fileURLToPath(import.meta.url)), "..", "skill", "SKILL.md");
1505
+ if (!existsSync2(src)) return;
1506
+ const dest = join3(skillDir, "SKILL.md");
1507
+ await copyFile(src, dest);
1508
+ } catch {
1509
+ }
1510
+ }
1511
+
1330
1512
  // src/index.ts
1513
+ var VERSION = "0.0.4";
1331
1514
  var program = new Command();
1332
- program.name("nrepo").description("NeuralRepo \u2014 capture and manage ideas from the terminal").version("0.0.2");
1515
+ program.name("nrepo").description("NeuralRepo \u2014 capture and manage ideas from the terminal").version(VERSION);
1333
1516
  program.command("login").description("Authenticate with NeuralRepo").option("--api-key", "Login with an API key instead of browser OAuth").action(wrap(loginCommand));
1334
1517
  program.command("logout").description("Clear stored credentials").action(wrap(async () => {
1335
1518
  await clearConfig();
@@ -1337,7 +1520,6 @@ program.command("logout").description("Clear stored credentials").action(wrap(as
1337
1520
  }));
1338
1521
  program.command("whoami").description("Show current user info").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(whoamiCommand));
1339
1522
  program.command("push <title>").description("Create a new idea").option("--body <text>", "Idea body/description").option("--tag <tag>", "Add tag (repeatable)", collect, []).option("--status <status>", "Initial status (captured|exploring|building|shipped|shelved)").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(pushCommand));
1340
- program.command("stash <title>").description("Quick-capture an idea (alias for push with defaults)").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(stashCommand));
1341
1523
  program.command("search <query>").description("Search ideas (semantic + full-text)").option("--limit <n>", "Max results").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(searchCommand));
1342
1524
  program.command("log").description("List recent ideas").option("--limit <n>", "Max results (default: 20)").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(logCommand));
1343
1525
  program.command("status").description("Overview dashboard").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(statusCommand));
@@ -1369,10 +1551,16 @@ program.command("unlink <source-id> <target-id>").description("Remove a link bet
1369
1551
  program.command("links <id>").description("Show all links for an idea").option("--type <type>", "Filter by link type").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(linksCommand));
1370
1552
  program.command("merge <keep-id> <absorb-id>").description("Merge two ideas (absorb the second into the first)").option("--force", "Skip confirmation").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(mergeCommand));
1371
1553
  program.command("graph <id>").description("Explore the connection graph from an idea").option("--depth <n>", "Max hops (default: 1, max: 5)").option("--type <types>", "Comma-separated edge types to follow").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(graphCommand));
1372
- var keys = program.command("keys").description("Manage API keys");
1554
+ program.command("rm <id>").description("Archive (soft-delete) an idea").option("--force", "Skip confirmation").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(rmCommand));
1555
+ var dup = program.command("duplicate").description("Manage duplicate detections");
1556
+ dup.command("list").description("List pending duplicate detections").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(duplicateListCommand));
1557
+ dup.command("dismiss <id>").description("Dismiss a duplicate detection").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(duplicateDismissCommand));
1558
+ dup.command("merge <id>").description("Merge duplicate into primary idea").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(duplicateMergeCommand));
1559
+ var keys = program.command("key").description("Manage API keys");
1373
1560
  keys.command("list").description("List all API keys").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(keysListCommand));
1374
1561
  keys.command("create <label>").description("Create a new API key").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(keysCreateCommand));
1375
1562
  keys.command("revoke <key-id>").description("Revoke an API key").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(keysRevokeCommand));
1563
+ checkForUpdates(VERSION);
1376
1564
  program.parse();
1377
1565
  function collect(value, previous) {
1378
1566
  return previous.concat([value]);
@@ -1404,21 +1592,21 @@ function wrap(fn) {
1404
1592
  process.exit(1);
1405
1593
  }
1406
1594
  if (err instanceof AuthError) {
1407
- console.error(chalk18.red(err.message));
1595
+ console.error(chalk21.red(err.message));
1408
1596
  process.exit(1);
1409
1597
  }
1410
1598
  if (err instanceof ApiError) {
1411
1599
  if (err.status === 401) {
1412
- console.error(chalk18.red("Authentication expired. Run `nrepo login` to re-authenticate."));
1600
+ console.error(chalk21.red("Authentication expired. Run `nrepo login` to re-authenticate."));
1413
1601
  } else if (err.status === 403) {
1414
- console.error(chalk18.yellow("This feature requires a Pro plan. Upgrade at https://neuralrepo.com/settings"));
1602
+ console.error(chalk21.yellow("This feature requires a Pro plan. Upgrade at https://neuralrepo.com/settings"));
1415
1603
  } else {
1416
- console.error(chalk18.red(`API error (${err.status}): ${err.message}`));
1604
+ console.error(chalk21.red(`API error (${err.status}): ${err.message}`));
1417
1605
  }
1418
1606
  process.exit(1);
1419
1607
  }
1420
1608
  if (err instanceof Error && err.message.startsWith("Network error")) {
1421
- console.error(chalk18.red(err.message));
1609
+ console.error(chalk21.red(err.message));
1422
1610
  process.exit(1);
1423
1611
  }
1424
1612
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralconfig/nrepo",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "NeuralRepo CLI — capture and manage ideas from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -40,8 +40,8 @@ nrepo push "New idea title" --body "Details here" --tag backend
40
40
  # Full capture with body and tags
41
41
  nrepo push "Add rate limiting to API" --body "Use sliding window algorithm, store in KV" --tag backend --tag infrastructure
42
42
 
43
- # Quick capture (no options)
44
- nrepo stash "Look into edge caching for static assets"
43
+ # Quick capture (title only — body and tags are optional)
44
+ nrepo push "Look into edge caching for static assets"
45
45
  ```
46
46
 
47
47
  ### Search and browse
@@ -173,11 +173,19 @@ nrepo graph 42 --depth 3 --type blocks
173
173
  nrepo move shipped --ids 91
174
174
  ```
175
175
 
176
- ### Identifying duplicates for merge
176
+ ### Managing duplicates
177
177
  ```bash
178
- nrepo diff 42 57
179
- # If they're duplicates, merge them
180
- nrepo merge 42 57 --force
178
+ nrepo duplicate # List pending duplicate detections
179
+ nrepo duplicate dismiss 7 # Dismiss a false positive
180
+ nrepo duplicate merge 7 # Merge duplicate into primary
181
+ nrepo diff 42 57 # Compare two ideas side-by-side
182
+ nrepo merge 42 57 --force # Manual merge if needed
183
+ ```
184
+
185
+ ### Archiving ideas
186
+ ```bash
187
+ nrepo rm 42 # Archive (soft-delete) an idea
188
+ nrepo rm 42 --force # Skip confirmation
181
189
  ```
182
190
 
183
191
  ### Building connections from search results