@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 +24 -6
- package/dist/index.js +254 -66
- package/package.json +1 -1
- package/skill/SKILL.md +14 -6
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`, `
|
|
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
|
|
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
|
|
236
|
-
nrepo
|
|
237
|
-
nrepo
|
|
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
|
|
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(
|
|
396
|
-
const score = chalk3.yellow(`${(
|
|
397
|
-
return ` ${chalk3.dim(`#${
|
|
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
|
|
545
|
-
console.log(formatDuplicate(
|
|
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/
|
|
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
|
|
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/
|
|
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 :
|
|
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(
|
|
1134
|
+
console.log(chalk17.green("\u2713") + ` Linked #${src} \u2192 #${tgt} (${relationType})`);
|
|
1047
1135
|
if (opts.note) {
|
|
1048
|
-
console.log(
|
|
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(
|
|
1144
|
+
console.error(chalk17.red(err.message));
|
|
1057
1145
|
if (!opts.force && (relationType === "supersedes" || relationType === "parent")) {
|
|
1058
|
-
console.error(
|
|
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 :
|
|
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 ${
|
|
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(
|
|
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 :
|
|
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(
|
|
1204
|
+
console.log(chalk17.bold(`Links for #${ideaId} "${idea.title}":`));
|
|
1117
1205
|
if (outgoing.length === 0 && incoming.length === 0) {
|
|
1118
|
-
console.log(
|
|
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(` ${
|
|
1232
|
+
console.log(` ${chalk17.bold(d.out)} ${d.arrow}`);
|
|
1145
1233
|
for (const r of items) {
|
|
1146
|
-
const status =
|
|
1147
|
-
const note = 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(` ${
|
|
1243
|
+
console.log(` ${chalk17.bold(d.in)} \u2190`);
|
|
1156
1244
|
for (const r of items) {
|
|
1157
|
-
const status =
|
|
1158
|
-
const note = 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
|
|
1167
|
-
import
|
|
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 :
|
|
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(
|
|
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(
|
|
1192
|
-
console.log(
|
|
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 :
|
|
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(
|
|
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
|
|
1210
|
-
import
|
|
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 :
|
|
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:
|
|
1271
|
-
exploring:
|
|
1272
|
-
building:
|
|
1273
|
-
shipped:
|
|
1274
|
-
shelved:
|
|
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:
|
|
1278
|
-
inspires:
|
|
1279
|
-
supersedes:
|
|
1280
|
-
parent:
|
|
1281
|
-
related:
|
|
1282
|
-
duplicate:
|
|
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] ??
|
|
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] ??
|
|
1386
|
+
const tc = typeColor[child.type] ?? chalk19.white;
|
|
1299
1387
|
const childNode = visited.get(child.childId);
|
|
1300
|
-
const childStyle = statusStyle3[childNode.status] ??
|
|
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] ??
|
|
1312
|
-
const gcTc = typeColor[gc.type] ??
|
|
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] ??
|
|
1410
|
+
const style = statusStyle3[root.status] ?? chalk19.white;
|
|
1323
1411
|
console.log(`#${root.id} ${root.title} ${style(`[${root.status}]`)}`);
|
|
1324
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1600
|
+
console.error(chalk21.red("Authentication expired. Run `nrepo login` to re-authenticate."));
|
|
1413
1601
|
} else if (err.status === 403) {
|
|
1414
|
-
console.error(
|
|
1602
|
+
console.error(chalk21.yellow("This feature requires a Pro plan. Upgrade at https://neuralrepo.com/settings"));
|
|
1415
1603
|
} else {
|
|
1416
|
-
console.error(
|
|
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(
|
|
1609
|
+
console.error(chalk21.red(err.message));
|
|
1422
1610
|
process.exit(1);
|
|
1423
1611
|
}
|
|
1424
1612
|
throw err;
|
package/package.json
CHANGED
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 (
|
|
44
|
-
nrepo
|
|
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
|
-
###
|
|
176
|
+
### Managing duplicates
|
|
177
177
|
```bash
|
|
178
|
-
nrepo
|
|
179
|
-
#
|
|
180
|
-
nrepo merge
|
|
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
|