@neuralconfig/nrepo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/index.js +1439 -0
- package/package.json +53 -0
- package/postinstall.js +20 -0
- package/preuninstall.js +11 -0
- package/skill/SKILL.md +204 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk18 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var CONFIG_DIR = join(homedir(), ".config", "neuralrepo");
|
|
13
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
|
+
var DEFAULT_API_URL = "https://neuralrepo.com/api/v1";
|
|
15
|
+
async function loadConfig() {
|
|
16
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(CONFIG_FILE, "utf-8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function saveConfig(config) {
|
|
25
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
26
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
29
|
+
}
|
|
30
|
+
async function clearConfig() {
|
|
31
|
+
if (existsSync(CONFIG_FILE)) {
|
|
32
|
+
await writeFile(CONFIG_FILE, "{}", "utf-8");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function getAuthenticatedConfig() {
|
|
36
|
+
const config = await loadConfig();
|
|
37
|
+
if (!config?.api_key) {
|
|
38
|
+
throw new AuthError("Not logged in. Run `nrepo login` to authenticate.");
|
|
39
|
+
}
|
|
40
|
+
return { ...config, api_url: config.api_url || DEFAULT_API_URL };
|
|
41
|
+
}
|
|
42
|
+
var AuthError = class extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "AuthError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/api.ts
|
|
50
|
+
var ApiError = class extends Error {
|
|
51
|
+
constructor(message, status, body) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.status = status;
|
|
54
|
+
this.body = body;
|
|
55
|
+
this.name = "ApiError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
async function request(config, method, path, body) {
|
|
59
|
+
const url = `${config.api_url}${path}`;
|
|
60
|
+
const headers = {
|
|
61
|
+
"X-API-Key": config.api_key,
|
|
62
|
+
"Content-Type": "application/json"
|
|
63
|
+
};
|
|
64
|
+
let res;
|
|
65
|
+
try {
|
|
66
|
+
res = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
headers,
|
|
69
|
+
body: body ? JSON.stringify(body) : void 0
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Network error: ${err.message}. Check your internet connection and try again.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const json = await res.json().catch(() => ({ error: res.statusText }));
|
|
78
|
+
throw new ApiError(
|
|
79
|
+
json.error ?? `HTTP ${res.status}`,
|
|
80
|
+
res.status,
|
|
81
|
+
json
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return res.json();
|
|
85
|
+
}
|
|
86
|
+
var getMe = (c) => request(c, "GET", "/user/me");
|
|
87
|
+
var listIdeas = (c, params) => {
|
|
88
|
+
const sp = new URLSearchParams();
|
|
89
|
+
if (params?.status) sp.set("status", params.status);
|
|
90
|
+
if (params?.tag) sp.set("tag", params.tag);
|
|
91
|
+
if (params?.limit) sp.set("limit", String(params.limit));
|
|
92
|
+
if (params?.offset) sp.set("offset", String(params.offset));
|
|
93
|
+
const qs = sp.toString();
|
|
94
|
+
return request(c, "GET", `/ideas${qs ? `?${qs}` : ""}`);
|
|
95
|
+
};
|
|
96
|
+
var createIdea = (c, data) => request(c, "POST", "/ideas", data);
|
|
97
|
+
var getIdea = (c, id) => request(c, "GET", `/ideas/${id}`);
|
|
98
|
+
var updateIdea = (c, id, data) => request(c, "PATCH", `/ideas/${id}`, data);
|
|
99
|
+
var searchIdeas = (c, query, limit) => {
|
|
100
|
+
const sp = new URLSearchParams({ q: query });
|
|
101
|
+
if (limit) sp.set("limit", String(limit));
|
|
102
|
+
return request(c, "GET", `/ideas/search?${sp.toString()}`);
|
|
103
|
+
};
|
|
104
|
+
var listDuplicates = (c) => request(c, "GET", "/ideas/duplicates");
|
|
105
|
+
var listApiKeys = (c) => request(c, "GET", "/user/api-keys");
|
|
106
|
+
var createApiKey = (c, label) => request(c, "POST", "/user/api-keys", { label });
|
|
107
|
+
var deleteApiKey = (c, keyId) => request(c, "DELETE", `/user/api-keys/${keyId}`);
|
|
108
|
+
var getIdeaRelations = (c, id) => request(c, "GET", `/ideas/${id}/relations`);
|
|
109
|
+
var createRelation = (c, sourceId, targetId, relationType = "related", note, force) => {
|
|
110
|
+
const qs = force ? "?force=true" : "";
|
|
111
|
+
return request(c, "POST", `/map/relations${qs}`, {
|
|
112
|
+
source_idea_id: sourceId,
|
|
113
|
+
target_idea_id: targetId,
|
|
114
|
+
relation_type: relationType,
|
|
115
|
+
...note ? { note } : {}
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
var deleteRelation = (c, relationId) => request(c, "DELETE", `/map/relations/${relationId}`);
|
|
119
|
+
var mergeIdeas = (c, keepId, absorbId) => request(c, "POST", `/ideas/${keepId}/merge`, { absorb_id: absorbId });
|
|
120
|
+
|
|
121
|
+
// src/commands/login.ts
|
|
122
|
+
import { createServer } from "http";
|
|
123
|
+
import { randomInt } from "crypto";
|
|
124
|
+
import { createInterface } from "readline/promises";
|
|
125
|
+
import chalk from "chalk";
|
|
126
|
+
import ora from "ora";
|
|
127
|
+
async function loginCommand(opts) {
|
|
128
|
+
if (opts.apiKey) {
|
|
129
|
+
await loginWithApiKey();
|
|
130
|
+
} else {
|
|
131
|
+
await loginWithBrowser();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function loginWithApiKey() {
|
|
135
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
136
|
+
const key = await rl.question(chalk.bold("Enter your API key: "));
|
|
137
|
+
rl.close();
|
|
138
|
+
if (!key.trim()) {
|
|
139
|
+
console.error(chalk.red("No API key provided."));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const spinner = ora("Verifying API key...").start();
|
|
143
|
+
const config = { api_url: DEFAULT_API_URL, api_key: key.trim() };
|
|
144
|
+
try {
|
|
145
|
+
const user = await getMe(config);
|
|
146
|
+
await saveConfig({ ...config, user_id: user.id, auth_method: "api-key" });
|
|
147
|
+
spinner.succeed(`Logged in as ${chalk.bold(user.display_name ?? user.email)} (${user.plan})`);
|
|
148
|
+
console.log(chalk.dim("Output defaults to JSON. Use --human for human-readable output."));
|
|
149
|
+
} catch {
|
|
150
|
+
spinner.fail("Invalid API key. Generate one at https://neuralrepo.com/settings");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function loginWithBrowser() {
|
|
155
|
+
const port = randomInt(49152, 65535);
|
|
156
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
157
|
+
console.log(chalk.dim("Starting local auth server..."));
|
|
158
|
+
const keyPromise = new Promise((resolve2, reject) => {
|
|
159
|
+
const timeout = setTimeout(() => {
|
|
160
|
+
server.close();
|
|
161
|
+
reject(new Error("Login timed out after 120 seconds"));
|
|
162
|
+
}, 12e4);
|
|
163
|
+
const server = createServer((req, res) => {
|
|
164
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
165
|
+
if (url.pathname === "/callback") {
|
|
166
|
+
const apiKey = url.searchParams.get("api_key");
|
|
167
|
+
if (apiKey) {
|
|
168
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
169
|
+
res.end(`
|
|
170
|
+
<html><head><meta charset="utf-8"></head><body style="font-family: sans-serif; text-align: center; padding: 60px; background: #0b0b0f; color: #fff;">
|
|
171
|
+
<h1>\u2713 Authenticated</h1>
|
|
172
|
+
<p>You can close this window and return to the terminal.</p>
|
|
173
|
+
</body></html>
|
|
174
|
+
`);
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
server.close();
|
|
177
|
+
resolve2(apiKey);
|
|
178
|
+
} else {
|
|
179
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
180
|
+
res.end("<html><body><h1>Error: No API key received</h1></body></html>");
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
server.close();
|
|
183
|
+
reject(new Error("No API key received in callback"));
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
res.writeHead(404);
|
|
187
|
+
res.end();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
server.listen(port, () => {
|
|
191
|
+
const authUrl = `${DEFAULT_API_URL.replace("/api/v1", "")}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
|
|
192
|
+
console.log(`
|
|
193
|
+
Open this URL to log in:
|
|
194
|
+
|
|
195
|
+
${chalk.underline(authUrl)}
|
|
196
|
+
`);
|
|
197
|
+
console.log(chalk.dim("Waiting for authentication..."));
|
|
198
|
+
const open = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
199
|
+
import("child_process").then(({ exec }) => {
|
|
200
|
+
exec(`${open} "${authUrl}"`, () => {
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const spinner = ora("Waiting for browser login...").start();
|
|
206
|
+
try {
|
|
207
|
+
const apiKey = await keyPromise;
|
|
208
|
+
const config = { api_url: DEFAULT_API_URL, api_key: apiKey };
|
|
209
|
+
const user = await getMe(config);
|
|
210
|
+
await saveConfig({ ...config, user_id: user.id, auth_method: "browser" });
|
|
211
|
+
spinner.succeed(`Logged in as ${chalk.bold(user.display_name ?? user.email)} (${user.plan})`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
spinner.fail(err.message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/commands/whoami.ts
|
|
219
|
+
import chalk2 from "chalk";
|
|
220
|
+
async function whoamiCommand(opts) {
|
|
221
|
+
const config = await getAuthenticatedConfig();
|
|
222
|
+
const user = await getMe(config);
|
|
223
|
+
if (opts.json) {
|
|
224
|
+
console.log(JSON.stringify(user, null, 2));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log(`${chalk2.bold(user.display_name ?? user.email)}`);
|
|
228
|
+
console.log(` Email: ${user.email}`);
|
|
229
|
+
console.log(` Plan: ${user.plan}`);
|
|
230
|
+
console.log(` ID: ${chalk2.dim(user.id)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/commands/push.ts
|
|
234
|
+
import chalk4 from "chalk";
|
|
235
|
+
import ora2 from "ora";
|
|
236
|
+
|
|
237
|
+
// src/format.ts
|
|
238
|
+
import chalk3 from "chalk";
|
|
239
|
+
|
|
240
|
+
// ../packages/shared/src/index.ts
|
|
241
|
+
import { z } from "zod";
|
|
242
|
+
var IDEA_STATUSES = ["captured", "exploring", "building", "shipped", "shelved"];
|
|
243
|
+
var IDEA_SOURCES = ["web", "cli", "claude-mcp", "siri", "email", "api", "shortcut"];
|
|
244
|
+
var LINK_TYPES = ["url", "claude-chat", "github-repo", "github-issue", "attachment"];
|
|
245
|
+
var RELATION_TYPES = ["related", "parent", "blocks", "inspires", "duplicate", "supersedes"];
|
|
246
|
+
var SOURCE_ICONS = {
|
|
247
|
+
"claude-mcp": "\u25C8",
|
|
248
|
+
siri: "\u25C9",
|
|
249
|
+
web: "\u25CE",
|
|
250
|
+
cli: "\u2B21",
|
|
251
|
+
email: "\u2709",
|
|
252
|
+
api: "\u2699",
|
|
253
|
+
shortcut: "\u2318"
|
|
254
|
+
};
|
|
255
|
+
var LIMITS = {
|
|
256
|
+
/** Max characters for idea title */
|
|
257
|
+
IDEA_TITLE_MAX: 200,
|
|
258
|
+
/** Max characters for idea body */
|
|
259
|
+
IDEA_BODY_MAX: 5e4,
|
|
260
|
+
/** Max tags per idea */
|
|
261
|
+
IDEA_TAGS_MAX: 20,
|
|
262
|
+
/** Max characters per tag name */
|
|
263
|
+
TAG_NAME_MAX: 50,
|
|
264
|
+
/** Max characters for a source URL */
|
|
265
|
+
SOURCE_URL_MAX: 2e3,
|
|
266
|
+
/** Max characters for display name */
|
|
267
|
+
DISPLAY_NAME_MAX: 100,
|
|
268
|
+
/** Max characters for API key label */
|
|
269
|
+
API_KEY_LABEL_MAX: 100,
|
|
270
|
+
/** Max characters for settings JSON blob */
|
|
271
|
+
SETTINGS_JSON_MAX: 1e4,
|
|
272
|
+
/** Max characters for search query */
|
|
273
|
+
SEARCH_QUERY_MAX: 500,
|
|
274
|
+
/** Max items per list request */
|
|
275
|
+
LIST_LIMIT_MAX: 100,
|
|
276
|
+
/** Default items per list request */
|
|
277
|
+
LIST_LIMIT_DEFAULT: 20
|
|
278
|
+
};
|
|
279
|
+
var UserSettingsSchema = z.object({
|
|
280
|
+
preferred_ai_provider: z.string().optional(),
|
|
281
|
+
search_threshold: z.number().min(0.1).max(0.9).optional(),
|
|
282
|
+
dedup_threshold: z.number().min(0.1).max(0.9).optional(),
|
|
283
|
+
related_threshold: z.number().min(0.1).max(0.9).optional()
|
|
284
|
+
});
|
|
285
|
+
var CreateIdeaSchema = z.object({
|
|
286
|
+
title: z.string().min(1).max(LIMITS.IDEA_TITLE_MAX),
|
|
287
|
+
body: z.string().max(LIMITS.IDEA_BODY_MAX).optional(),
|
|
288
|
+
tags: z.array(z.string().min(1).max(LIMITS.TAG_NAME_MAX)).max(LIMITS.IDEA_TAGS_MAX).optional(),
|
|
289
|
+
source: z.enum(IDEA_SOURCES).optional(),
|
|
290
|
+
source_url: z.string().url().max(LIMITS.SOURCE_URL_MAX).optional(),
|
|
291
|
+
status: z.enum(IDEA_STATUSES).optional(),
|
|
292
|
+
parent_id: z.number().int().positive().optional()
|
|
293
|
+
});
|
|
294
|
+
var UpdateIdeaSchema = z.object({
|
|
295
|
+
title: z.string().min(1).max(LIMITS.IDEA_TITLE_MAX).optional(),
|
|
296
|
+
body: z.string().max(LIMITS.IDEA_BODY_MAX).optional(),
|
|
297
|
+
status: z.enum(IDEA_STATUSES).optional(),
|
|
298
|
+
parent_id: z.number().int().positive().nullable().optional(),
|
|
299
|
+
tags: z.array(z.string().min(1).max(LIMITS.TAG_NAME_MAX)).max(LIMITS.IDEA_TAGS_MAX).optional()
|
|
300
|
+
});
|
|
301
|
+
var CreateTagSchema = z.object({
|
|
302
|
+
name: z.string().min(1).max(LIMITS.TAG_NAME_MAX),
|
|
303
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
|
|
304
|
+
});
|
|
305
|
+
var UpdateTagSchema = z.object({
|
|
306
|
+
name: z.string().min(1).max(LIMITS.TAG_NAME_MAX).optional(),
|
|
307
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
|
|
308
|
+
});
|
|
309
|
+
var UpdateProfileSchema = z.object({
|
|
310
|
+
display_name: z.string().min(1).max(LIMITS.DISPLAY_NAME_MAX).optional(),
|
|
311
|
+
settings_json: z.string().max(LIMITS.SETTINGS_JSON_MAX).optional()
|
|
312
|
+
});
|
|
313
|
+
var CreateApiKeySchema = z.object({
|
|
314
|
+
label: z.string().min(1).max(LIMITS.API_KEY_LABEL_MAX).optional().default("default")
|
|
315
|
+
});
|
|
316
|
+
var CreateRelationSchema = z.object({
|
|
317
|
+
source_idea_id: z.number().int().positive(),
|
|
318
|
+
target_idea_id: z.number().int().positive(),
|
|
319
|
+
relation_type: z.enum(RELATION_TYPES).default("related"),
|
|
320
|
+
note: z.string().max(500).optional()
|
|
321
|
+
});
|
|
322
|
+
var UpdateRelationSchema = z.object({
|
|
323
|
+
relation_type: z.enum(RELATION_TYPES).optional(),
|
|
324
|
+
note: z.string().max(500).nullable().optional()
|
|
325
|
+
});
|
|
326
|
+
var MergeIdeasSchema = z.object({
|
|
327
|
+
absorb_id: z.number().int().positive()
|
|
328
|
+
});
|
|
329
|
+
var CreateUrlLinkSchema = z.object({
|
|
330
|
+
url: z.string().url().max(LIMITS.SOURCE_URL_MAX),
|
|
331
|
+
title: z.string().max(LIMITS.IDEA_TITLE_MAX).optional(),
|
|
332
|
+
link_type: z.enum(LINK_TYPES).default("url")
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/format.ts
|
|
336
|
+
var statusStyle = {
|
|
337
|
+
captured: chalk3.gray,
|
|
338
|
+
exploring: chalk3.cyan,
|
|
339
|
+
building: chalk3.yellow,
|
|
340
|
+
shipped: chalk3.green,
|
|
341
|
+
shelved: chalk3.dim
|
|
342
|
+
};
|
|
343
|
+
function formatIdeaRow(idea) {
|
|
344
|
+
const style = statusStyle[idea.status] ?? chalk3.white;
|
|
345
|
+
const icon = SOURCE_ICONS[idea.source] ?? "\xB7";
|
|
346
|
+
const tags = idea.tags.length ? chalk3.dim(` [${idea.tags.join(", ")}]`) : "";
|
|
347
|
+
const score = idea.score != null ? chalk3.dim(` (${(idea.score * 100).toFixed(0)}%)`) : "";
|
|
348
|
+
const id = chalk3.dim(`#${idea.id}`);
|
|
349
|
+
const status = style(idea.status.padEnd(10));
|
|
350
|
+
return `${id} ${status} ${icon} ${idea.title}${tags}${score}`;
|
|
351
|
+
}
|
|
352
|
+
function formatIdeaDetail(idea) {
|
|
353
|
+
const lines = [];
|
|
354
|
+
const style = statusStyle[idea.status] ?? chalk3.white;
|
|
355
|
+
const icon = SOURCE_ICONS[idea.source] ?? "\xB7";
|
|
356
|
+
lines.push(chalk3.bold(`#${idea.id} ${idea.title}`));
|
|
357
|
+
lines.push("");
|
|
358
|
+
lines.push(` Status: ${style(idea.status)}`);
|
|
359
|
+
lines.push(` Source: ${icon} ${idea.source}`);
|
|
360
|
+
lines.push(` Created: ${formatDate(idea.created_at)}`);
|
|
361
|
+
lines.push(` Updated: ${formatDate(idea.updated_at)}`);
|
|
362
|
+
if (idea.tags.length) {
|
|
363
|
+
lines.push(` Tags: ${idea.tags.map((t) => chalk3.cyan(t)).join(", ")}`);
|
|
364
|
+
}
|
|
365
|
+
if (idea.source_url) {
|
|
366
|
+
lines.push(` URL: ${chalk3.underline(idea.source_url)}`);
|
|
367
|
+
}
|
|
368
|
+
if (idea.body) {
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push(chalk3.dim(" \u2500".repeat(30)));
|
|
371
|
+
lines.push("");
|
|
372
|
+
for (const line of idea.body.split("\n")) {
|
|
373
|
+
lines.push(` ${line}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (idea.links?.length) {
|
|
377
|
+
lines.push("");
|
|
378
|
+
lines.push(chalk3.bold(" Links"));
|
|
379
|
+
for (const link of idea.links) {
|
|
380
|
+
const label = link.title ?? link.link_type;
|
|
381
|
+
lines.push(` ${chalk3.dim("\xB7")} ${label}: ${chalk3.underline(link.url)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (idea.relations?.length) {
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push(chalk3.bold(" Related Ideas"));
|
|
387
|
+
for (const rel of idea.relations) {
|
|
388
|
+
const relScore = rel.score != null ? chalk3.dim(` (${(rel.score * 100).toFixed(0)}%)`) : "";
|
|
389
|
+
const title = rel.related_idea_title ?? `#${rel.target_idea_id}`;
|
|
390
|
+
lines.push(` ${chalk3.dim("\xB7")} ${rel.relation_type}: ${title}${relScore}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return lines.join("\n");
|
|
394
|
+
}
|
|
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
|
+
}
|
|
399
|
+
function formatDate(iso) {
|
|
400
|
+
const normalized = iso.includes("T") || iso.includes("Z") ? iso : iso.replace(" ", "T") + "Z";
|
|
401
|
+
const d = new Date(normalized);
|
|
402
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
403
|
+
}
|
|
404
|
+
function formatStatusCounts(ideas) {
|
|
405
|
+
const counts = {
|
|
406
|
+
captured: 0,
|
|
407
|
+
exploring: 0,
|
|
408
|
+
building: 0,
|
|
409
|
+
shipped: 0,
|
|
410
|
+
shelved: 0
|
|
411
|
+
};
|
|
412
|
+
for (const idea of ideas) {
|
|
413
|
+
counts[idea.status] = (counts[idea.status] ?? 0) + 1;
|
|
414
|
+
}
|
|
415
|
+
return counts;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/commands/push.ts
|
|
419
|
+
async function pushCommand(title, opts) {
|
|
420
|
+
const config = await getAuthenticatedConfig();
|
|
421
|
+
const spinner = opts.json ? null : ora2("Creating idea...").start();
|
|
422
|
+
const idea = await createIdea(config, {
|
|
423
|
+
title,
|
|
424
|
+
body: opts.body,
|
|
425
|
+
tags: opts.tag,
|
|
426
|
+
source: "cli",
|
|
427
|
+
status: opts.status
|
|
428
|
+
});
|
|
429
|
+
spinner?.stop();
|
|
430
|
+
if (opts.json) {
|
|
431
|
+
console.log(JSON.stringify(idea, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.log(chalk4.green("\u2713") + " Idea captured");
|
|
435
|
+
console.log(formatIdeaRow(idea));
|
|
436
|
+
if (idea.processing) {
|
|
437
|
+
console.log(chalk4.dim("\n Processing: embeddings, dedup, and auto-tagging queued"));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function stashCommand(title, opts) {
|
|
441
|
+
await pushCommand(title, { json: opts.json });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/commands/search.ts
|
|
445
|
+
import chalk5 from "chalk";
|
|
446
|
+
import ora3 from "ora";
|
|
447
|
+
async function searchCommand(query, opts) {
|
|
448
|
+
const config = await getAuthenticatedConfig();
|
|
449
|
+
const spinner = opts.json ? null : ora3("Searching...").start();
|
|
450
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
451
|
+
const data = await searchIdeas(config, query, limit);
|
|
452
|
+
spinner?.stop();
|
|
453
|
+
if (opts.json) {
|
|
454
|
+
console.log(JSON.stringify(data, null, 2));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
console.log(chalk5.dim(`Search: "${query}" (${data.search_type}) \u2014 ${data.results.length} results
|
|
458
|
+
`));
|
|
459
|
+
if (data.results.length === 0) {
|
|
460
|
+
console.log(chalk5.dim(" No results found."));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
for (const idea of data.results) {
|
|
464
|
+
console.log(formatIdeaRow(idea));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/commands/log.ts
|
|
469
|
+
import chalk6 from "chalk";
|
|
470
|
+
import ora4 from "ora";
|
|
471
|
+
async function logCommand(opts) {
|
|
472
|
+
const config = await getAuthenticatedConfig();
|
|
473
|
+
const spinner = opts.json ? null : ora4("Loading ideas...").start();
|
|
474
|
+
const data = await listIdeas(config, {
|
|
475
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : 20,
|
|
476
|
+
status: opts.status,
|
|
477
|
+
tag: opts.tag
|
|
478
|
+
});
|
|
479
|
+
spinner?.stop();
|
|
480
|
+
if (opts.json) {
|
|
481
|
+
console.log(JSON.stringify(data.ideas, null, 2));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (data.ideas.length === 0) {
|
|
485
|
+
console.log(chalk6.dim("No ideas found."));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const idea of data.ideas) {
|
|
489
|
+
console.log(formatIdeaRow(idea));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/status.ts
|
|
494
|
+
import chalk7 from "chalk";
|
|
495
|
+
import ora5 from "ora";
|
|
496
|
+
var statusStyle2 = {
|
|
497
|
+
captured: chalk7.gray,
|
|
498
|
+
exploring: chalk7.cyan,
|
|
499
|
+
building: chalk7.yellow,
|
|
500
|
+
shipped: chalk7.green,
|
|
501
|
+
shelved: chalk7.dim
|
|
502
|
+
};
|
|
503
|
+
async function statusCommand(opts) {
|
|
504
|
+
const config = await getAuthenticatedConfig();
|
|
505
|
+
const spinner = opts.json ? null : ora5("Loading dashboard...").start();
|
|
506
|
+
const [ideasData, dupsData, user] = await Promise.all([
|
|
507
|
+
listIdeas(config, { limit: 100 }),
|
|
508
|
+
listDuplicates(config),
|
|
509
|
+
getMe(config)
|
|
510
|
+
]);
|
|
511
|
+
spinner?.stop();
|
|
512
|
+
if (opts.json) {
|
|
513
|
+
console.log(JSON.stringify({
|
|
514
|
+
user: { email: user.email, plan: user.plan },
|
|
515
|
+
counts: formatStatusCounts(ideasData.ideas),
|
|
516
|
+
total: ideasData.ideas.length,
|
|
517
|
+
pending_duplicates: dupsData.duplicates.length
|
|
518
|
+
}, null, 2));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
console.log(chalk7.bold("NeuralRepo Dashboard"));
|
|
522
|
+
console.log(chalk7.dim(`${user.display_name ?? user.email} \xB7 ${user.plan}
|
|
523
|
+
`));
|
|
524
|
+
const counts = formatStatusCounts(ideasData.ideas);
|
|
525
|
+
console.log(chalk7.bold("Status breakdown"));
|
|
526
|
+
for (const [status, count] of Object.entries(counts)) {
|
|
527
|
+
const style = statusStyle2[status] ?? chalk7.white;
|
|
528
|
+
const bar = "\u2588".repeat(Math.min(count, 40));
|
|
529
|
+
console.log(` ${style(status.padEnd(10))} ${style(bar)} ${count}`);
|
|
530
|
+
}
|
|
531
|
+
console.log(` ${"total".padEnd(10)} ${chalk7.bold(String(ideasData.ideas.length))}
|
|
532
|
+
`);
|
|
533
|
+
const recent = ideasData.ideas.filter((i) => i.status === "captured").slice(0, 5);
|
|
534
|
+
if (recent.length > 0) {
|
|
535
|
+
console.log(chalk7.bold("Recent captures"));
|
|
536
|
+
for (const idea of recent) {
|
|
537
|
+
console.log(` ${formatIdeaRow(idea)}`);
|
|
538
|
+
}
|
|
539
|
+
console.log("");
|
|
540
|
+
}
|
|
541
|
+
const pendingDups = dupsData.duplicates.filter((d) => d.status === "pending");
|
|
542
|
+
if (pendingDups.length > 0) {
|
|
543
|
+
console.log(chalk7.bold(`Pending duplicates (${pendingDups.length})`));
|
|
544
|
+
for (const dup of pendingDups.slice(0, 5)) {
|
|
545
|
+
console.log(formatDuplicate(dup));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/commands/show.ts
|
|
551
|
+
import ora6 from "ora";
|
|
552
|
+
async function showCommand(id, opts) {
|
|
553
|
+
const config = await getAuthenticatedConfig();
|
|
554
|
+
const ideaId = parseInt(id, 10);
|
|
555
|
+
if (isNaN(ideaId)) {
|
|
556
|
+
console.error("Invalid idea ID");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const spinner = opts.json ? null : ora6("Loading idea...").start();
|
|
560
|
+
const idea = await getIdea(config, ideaId);
|
|
561
|
+
spinner?.stop();
|
|
562
|
+
if (opts.json) {
|
|
563
|
+
console.log(JSON.stringify(idea, null, 2));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
console.log(formatIdeaDetail(idea));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/commands/move.ts
|
|
570
|
+
import chalk8 from "chalk";
|
|
571
|
+
import ora7 from "ora";
|
|
572
|
+
async function moveCommand(id, status, opts) {
|
|
573
|
+
if (!IDEA_STATUSES.includes(status)) {
|
|
574
|
+
console.error(`Invalid status "${status}". Must be one of: ${IDEA_STATUSES.join(", ")}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const config = await getAuthenticatedConfig();
|
|
578
|
+
const ideaId = parseInt(id, 10);
|
|
579
|
+
if (isNaN(ideaId)) {
|
|
580
|
+
console.error("Invalid idea ID");
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const spinner = opts.json ? null : ora7("Updating status...").start();
|
|
584
|
+
const idea = await updateIdea(config, ideaId, { status });
|
|
585
|
+
spinner?.stop();
|
|
586
|
+
if (opts.json) {
|
|
587
|
+
console.log(JSON.stringify(idea, null, 2));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.log(chalk8.green("\u2713") + ` #${idea.id} \u2192 ${status}`);
|
|
591
|
+
}
|
|
592
|
+
async function moveBulkCommand(status, opts) {
|
|
593
|
+
if (!IDEA_STATUSES.includes(status)) {
|
|
594
|
+
console.error(`Invalid status "${status}". Must be one of: ${IDEA_STATUSES.join(", ")}`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const config = await getAuthenticatedConfig();
|
|
598
|
+
const ids = opts.ids.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n > 0);
|
|
599
|
+
if (ids.length === 0) {
|
|
600
|
+
console.error("Provide at least one ID with --ids");
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
const spinner = opts.json ? null : ora7(`Moving ${ids.length} ideas to ${status}...`).start();
|
|
604
|
+
const results = await Promise.allSettled(
|
|
605
|
+
ids.map(async (id) => {
|
|
606
|
+
const idea = await updateIdea(config, id, { status });
|
|
607
|
+
return { id, title: idea.title };
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
spinner?.stop();
|
|
611
|
+
if (opts.json) {
|
|
612
|
+
const output = results.map((r, i) => ({
|
|
613
|
+
id: ids[i],
|
|
614
|
+
success: r.status === "fulfilled",
|
|
615
|
+
error: r.status === "rejected" ? r.reason.message : void 0
|
|
616
|
+
}));
|
|
617
|
+
console.log(JSON.stringify(output, null, 2));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
console.log(`Moved ${ids.length} ideas to ${status}:`);
|
|
621
|
+
results.forEach((r, i) => {
|
|
622
|
+
if (r.status === "fulfilled") {
|
|
623
|
+
console.log(` ${chalk8.green("\u2713")} #${ids[i]} ${r.value.title}`);
|
|
624
|
+
} else {
|
|
625
|
+
console.log(` ${chalk8.red("\u2717")} #${ids[i]} ${r.reason.message}`);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/commands/tag.ts
|
|
631
|
+
import chalk9 from "chalk";
|
|
632
|
+
import ora8 from "ora";
|
|
633
|
+
async function tagCommand(id, tags, opts) {
|
|
634
|
+
if (tags.length === 0) {
|
|
635
|
+
console.error("Provide at least one tag");
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
const config = await getAuthenticatedConfig();
|
|
639
|
+
const ideaId = parseInt(id, 10);
|
|
640
|
+
if (isNaN(ideaId)) {
|
|
641
|
+
console.error("Invalid idea ID");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
const spinner = opts.json ? null : ora8("Updating tags...").start();
|
|
645
|
+
const existing = await getIdea(config, ideaId);
|
|
646
|
+
const merged = [.../* @__PURE__ */ new Set([...existing.tags, ...tags])];
|
|
647
|
+
const idea = await updateIdea(config, ideaId, { tags: merged });
|
|
648
|
+
spinner?.stop();
|
|
649
|
+
if (opts.json) {
|
|
650
|
+
console.log(JSON.stringify(idea, null, 2));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
console.log(chalk9.green("\u2713") + ` #${idea.id} tags: ${idea.tags.join(", ")}`);
|
|
654
|
+
}
|
|
655
|
+
async function tagAddCommand(tag, opts) {
|
|
656
|
+
const config = await getAuthenticatedConfig();
|
|
657
|
+
const ids = parseIds(opts.ids);
|
|
658
|
+
if (ids.length === 0) {
|
|
659
|
+
console.error("Provide at least one ID with --ids");
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
const spinner = opts.json ? null : ora8(`Adding tag "${tag}" to ${ids.length} ideas...`).start();
|
|
663
|
+
const results = await Promise.allSettled(
|
|
664
|
+
ids.map(async (id) => {
|
|
665
|
+
const existing = await getIdea(config, id);
|
|
666
|
+
const merged = [.../* @__PURE__ */ new Set([...existing.tags, tag])];
|
|
667
|
+
await updateIdea(config, id, { tags: merged });
|
|
668
|
+
return { id, title: existing.title };
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
spinner?.stop();
|
|
672
|
+
if (opts.json) {
|
|
673
|
+
const output = results.map((r, i) => ({
|
|
674
|
+
id: ids[i],
|
|
675
|
+
success: r.status === "fulfilled",
|
|
676
|
+
error: r.status === "rejected" ? r.reason.message : void 0
|
|
677
|
+
}));
|
|
678
|
+
console.log(JSON.stringify(output, null, 2));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
console.log(`Tagged ${ids.length} ideas with "${tag}":`);
|
|
682
|
+
results.forEach((r, i) => {
|
|
683
|
+
if (r.status === "fulfilled") {
|
|
684
|
+
console.log(` ${chalk9.green("\u2713")} #${ids[i]} ${r.value.title}`);
|
|
685
|
+
} else {
|
|
686
|
+
console.log(` ${chalk9.red("\u2717")} #${ids[i]} ${r.reason.message}`);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async function tagRemoveCommand(tag, opts) {
|
|
691
|
+
const config = await getAuthenticatedConfig();
|
|
692
|
+
const ids = parseIds(opts.ids);
|
|
693
|
+
if (ids.length === 0) {
|
|
694
|
+
console.error("Provide at least one ID with --ids");
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
const spinner = opts.json ? null : ora8(`Removing tag "${tag}" from ${ids.length} ideas...`).start();
|
|
698
|
+
const results = await Promise.allSettled(
|
|
699
|
+
ids.map(async (id) => {
|
|
700
|
+
const existing = await getIdea(config, id);
|
|
701
|
+
const filtered = existing.tags.filter((t) => t !== tag);
|
|
702
|
+
await updateIdea(config, id, { tags: filtered });
|
|
703
|
+
return { id, title: existing.title };
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
spinner?.stop();
|
|
707
|
+
if (opts.json) {
|
|
708
|
+
const output = results.map((r, i) => ({
|
|
709
|
+
id: ids[i],
|
|
710
|
+
success: r.status === "fulfilled",
|
|
711
|
+
error: r.status === "rejected" ? r.reason.message : void 0
|
|
712
|
+
}));
|
|
713
|
+
console.log(JSON.stringify(output, null, 2));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
console.log(`Removed tag "${tag}" from ${ids.length} ideas:`);
|
|
717
|
+
results.forEach((r, i) => {
|
|
718
|
+
if (r.status === "fulfilled") {
|
|
719
|
+
console.log(` ${chalk9.green("\u2713")} #${ids[i]} ${r.value.title}`);
|
|
720
|
+
} else {
|
|
721
|
+
console.log(` ${chalk9.red("\u2717")} #${ids[i]} ${r.reason.message}`);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function parseIds(idsStr) {
|
|
726
|
+
return idsStr.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n > 0);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/commands/pull.ts
|
|
730
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
731
|
+
import { join as join2, resolve } from "path";
|
|
732
|
+
import chalk10 from "chalk";
|
|
733
|
+
import ora9 from "ora";
|
|
734
|
+
async function pullCommand(id, opts) {
|
|
735
|
+
const config = await getAuthenticatedConfig();
|
|
736
|
+
const ideaId = parseInt(id, 10);
|
|
737
|
+
if (isNaN(ideaId)) {
|
|
738
|
+
console.error("Invalid idea ID");
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const spinner = opts.json ? null : ora9("Pulling idea context...").start();
|
|
742
|
+
const idea = await getIdea(config, ideaId);
|
|
743
|
+
spinner?.stop();
|
|
744
|
+
if (opts.json) {
|
|
745
|
+
console.log(JSON.stringify(idea, null, 2));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const dir = resolve(opts.to ?? ".");
|
|
749
|
+
await mkdir2(dir, { recursive: true });
|
|
750
|
+
const ideaMd = [
|
|
751
|
+
`# ${idea.title}`,
|
|
752
|
+
"",
|
|
753
|
+
`**Status:** ${idea.status}`,
|
|
754
|
+
`**Source:** ${idea.source}`,
|
|
755
|
+
`**Created:** ${idea.created_at}`,
|
|
756
|
+
idea.tags.length ? `**Tags:** ${idea.tags.join(", ")}` : "",
|
|
757
|
+
idea.source_url ? `**URL:** ${idea.source_url}` : "",
|
|
758
|
+
"",
|
|
759
|
+
"---",
|
|
760
|
+
"",
|
|
761
|
+
idea.body ?? "_No body_"
|
|
762
|
+
].filter(Boolean).join("\n");
|
|
763
|
+
await writeFile2(join2(dir, "IDEA.md"), ideaMd + "\n", "utf-8");
|
|
764
|
+
const relations = idea.relations ?? [];
|
|
765
|
+
if (relations.length > 0) {
|
|
766
|
+
const contextLines = [
|
|
767
|
+
"# Related Ideas",
|
|
768
|
+
"",
|
|
769
|
+
...relations.map((r) => {
|
|
770
|
+
const score = r.score != null ? ` (${(r.score * 100).toFixed(0)}%)` : "";
|
|
771
|
+
const title = r.related_idea_title ?? `#${r.target_idea_id}`;
|
|
772
|
+
return `- **${r.relation_type}**: ${title}${score}`;
|
|
773
|
+
})
|
|
774
|
+
];
|
|
775
|
+
await writeFile2(join2(dir, "CONTEXT.md"), contextLines.join("\n") + "\n", "utf-8");
|
|
776
|
+
}
|
|
777
|
+
const links = idea.links ?? [];
|
|
778
|
+
if (links.length > 0) {
|
|
779
|
+
const linkLines = [
|
|
780
|
+
"# Links",
|
|
781
|
+
"",
|
|
782
|
+
...links.map((l) => `- [${l.title ?? l.link_type}](${l.url})`)
|
|
783
|
+
];
|
|
784
|
+
await writeFile2(join2(dir, "RELATED.md"), linkLines.join("\n") + "\n", "utf-8");
|
|
785
|
+
}
|
|
786
|
+
const syncConfig = {
|
|
787
|
+
idea_id: idea.id,
|
|
788
|
+
api_url: config.api_url,
|
|
789
|
+
pulled_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
790
|
+
};
|
|
791
|
+
await writeFile2(join2(dir, ".neuralrepo"), JSON.stringify(syncConfig, null, 2) + "\n", "utf-8");
|
|
792
|
+
console.log(chalk10.green("\u2713") + ` Pulled #${idea.id} to ${dir}/`);
|
|
793
|
+
console.log(chalk10.dim(` IDEA.md${relations.length ? ", CONTEXT.md" : ""}${links.length ? ", RELATED.md" : ""}, .neuralrepo`));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/commands/diff.ts
|
|
797
|
+
import chalk11 from "chalk";
|
|
798
|
+
import ora10 from "ora";
|
|
799
|
+
async function diffCommand(id1, id2OrOpts, opts) {
|
|
800
|
+
const config = await getAuthenticatedConfig();
|
|
801
|
+
const firstId = parseInt(id1, 10);
|
|
802
|
+
if (isNaN(firstId)) {
|
|
803
|
+
console.error("Invalid idea ID");
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
let secondId;
|
|
807
|
+
let resolvedOpts;
|
|
808
|
+
if (typeof id2OrOpts === "string") {
|
|
809
|
+
secondId = parseInt(id2OrOpts, 10);
|
|
810
|
+
if (isNaN(secondId)) {
|
|
811
|
+
console.error("Invalid second idea ID");
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
resolvedOpts = opts ?? {};
|
|
815
|
+
} else if (id2OrOpts && typeof id2OrOpts === "object") {
|
|
816
|
+
resolvedOpts = id2OrOpts;
|
|
817
|
+
} else {
|
|
818
|
+
resolvedOpts = opts ?? {};
|
|
819
|
+
}
|
|
820
|
+
const jsonOutput = resolvedOpts.json ?? false;
|
|
821
|
+
const spinner = jsonOutput ? null : ora10("Loading ideas...").start();
|
|
822
|
+
const ideaA = await getIdea(config, firstId);
|
|
823
|
+
if (secondId == null) {
|
|
824
|
+
secondId = findComparisonTarget(ideaA);
|
|
825
|
+
if (secondId == null) {
|
|
826
|
+
spinner?.stop();
|
|
827
|
+
if (jsonOutput) {
|
|
828
|
+
console.error(JSON.stringify({ error: `No parent or related idea to diff against. Usage: nrepo diff ${firstId} <other-id>`, code: "no_diff_target" }));
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
console.log(chalk11.dim("No parent or related idea to diff against."));
|
|
832
|
+
console.log(chalk11.dim(`Usage: nrepo diff ${firstId} <other-id>`));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const ideaB = await getIdea(config, secondId);
|
|
837
|
+
spinner?.stop();
|
|
838
|
+
if (jsonOutput) {
|
|
839
|
+
console.log(JSON.stringify({ a: ideaA, b: ideaB, diff: computeDiff(ideaA, ideaB) }, null, 2));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
printDiff(ideaA, ideaB);
|
|
843
|
+
}
|
|
844
|
+
function findComparisonTarget(idea) {
|
|
845
|
+
if (idea.parent_id) return idea.parent_id;
|
|
846
|
+
if (!idea.relations?.length) return void 0;
|
|
847
|
+
const parent = idea.relations.find((r) => r.relation_type === "parent");
|
|
848
|
+
if (parent) return parent.target_idea_id;
|
|
849
|
+
const duplicate = idea.relations.find((r) => r.relation_type === "duplicate");
|
|
850
|
+
if (duplicate) return duplicate.target_idea_id;
|
|
851
|
+
const sorted = [...idea.relations].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
852
|
+
return sorted[0]?.target_idea_id;
|
|
853
|
+
}
|
|
854
|
+
function computeDiff(a, b) {
|
|
855
|
+
const fields = [
|
|
856
|
+
{ field: "Title", key: "title" },
|
|
857
|
+
{ field: "Body", key: "body" },
|
|
858
|
+
{ field: "Status", key: "status" },
|
|
859
|
+
{ field: "Source", key: "source" },
|
|
860
|
+
{ field: "Tags", key: "tags" }
|
|
861
|
+
];
|
|
862
|
+
return fields.map(({ field, key }) => {
|
|
863
|
+
const valA = key === "tags" ? (a.tags ?? []).join(", ") : String(a[key] ?? "");
|
|
864
|
+
const valB = key === "tags" ? (b.tags ?? []).join(", ") : String(b[key] ?? "");
|
|
865
|
+
return { field, a: valA, b: valB, changed: valA !== valB };
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function printDiff(a, b) {
|
|
869
|
+
const diffs = computeDiff(a, b);
|
|
870
|
+
const anyChanged = diffs.some((d) => d.changed);
|
|
871
|
+
console.log(chalk11.bold(`diff #${a.id} \u2192 #${b.id}`));
|
|
872
|
+
console.log(chalk11.dim("\u2500".repeat(60)));
|
|
873
|
+
if (!anyChanged) {
|
|
874
|
+
console.log(chalk11.dim(" No differences found."));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
for (const d of diffs) {
|
|
878
|
+
if (!d.changed) continue;
|
|
879
|
+
console.log(chalk11.bold(`
|
|
880
|
+
${d.field}`));
|
|
881
|
+
if (d.field === "Body") {
|
|
882
|
+
printBodyDiff(d.a, d.b);
|
|
883
|
+
} else {
|
|
884
|
+
console.log(chalk11.red(` - ${d.a || "(empty)"}`));
|
|
885
|
+
console.log(chalk11.green(` + ${d.b || "(empty)"}`));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
console.log("");
|
|
889
|
+
}
|
|
890
|
+
function printBodyDiff(bodyA, bodyB) {
|
|
891
|
+
const linesA = (bodyA || "").split("\n");
|
|
892
|
+
const linesB = (bodyB || "").split("\n");
|
|
893
|
+
const maxLen = Math.max(linesA.length, linesB.length);
|
|
894
|
+
for (let i = 0; i < maxLen; i++) {
|
|
895
|
+
const lineA = linesA[i];
|
|
896
|
+
const lineB = linesB[i];
|
|
897
|
+
if (lineA === lineB) {
|
|
898
|
+
console.log(chalk11.dim(` ${lineA}`));
|
|
899
|
+
} else {
|
|
900
|
+
if (lineA != null) console.log(chalk11.red(` - ${lineA}`));
|
|
901
|
+
if (lineB != null) console.log(chalk11.green(` + ${lineB}`));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/commands/branch.ts
|
|
907
|
+
import chalk12 from "chalk";
|
|
908
|
+
import ora11 from "ora";
|
|
909
|
+
async function branchCommand(id, opts) {
|
|
910
|
+
const config = await getAuthenticatedConfig();
|
|
911
|
+
const sourceId = parseInt(id, 10);
|
|
912
|
+
if (isNaN(sourceId)) {
|
|
913
|
+
console.error("Invalid idea ID");
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const spinner = opts.json ? null : ora11("Branching idea...").start();
|
|
917
|
+
const source = await getIdea(config, sourceId);
|
|
918
|
+
const forked = await createIdea(config, {
|
|
919
|
+
title: opts.title ?? source.title,
|
|
920
|
+
body: opts.body ?? source.body ?? void 0,
|
|
921
|
+
tags: source.tags,
|
|
922
|
+
source: "cli",
|
|
923
|
+
status: "captured",
|
|
924
|
+
parent_id: sourceId
|
|
925
|
+
});
|
|
926
|
+
spinner?.stop();
|
|
927
|
+
if (opts.json) {
|
|
928
|
+
console.log(JSON.stringify(forked, null, 2));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
console.log(chalk12.green("\u2713") + ` Branched from #${sourceId}`);
|
|
932
|
+
console.log(formatIdeaRow(forked));
|
|
933
|
+
if (forked.processing) {
|
|
934
|
+
console.log(chalk12.dim("\n Processing: embeddings, dedup, and auto-tagging queued"));
|
|
935
|
+
}
|
|
936
|
+
console.log(chalk12.dim(`
|
|
937
|
+
Compare with: nrepo diff ${sourceId} ${forked.id}`));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/commands/edit.ts
|
|
941
|
+
import chalk13 from "chalk";
|
|
942
|
+
import ora12 from "ora";
|
|
943
|
+
async function editCommand(id, opts) {
|
|
944
|
+
const config = await getAuthenticatedConfig();
|
|
945
|
+
const ideaId = parseInt(id, 10);
|
|
946
|
+
if (isNaN(ideaId)) {
|
|
947
|
+
console.error("Invalid idea ID");
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
const updates = {};
|
|
951
|
+
if (opts.title) updates.title = opts.title;
|
|
952
|
+
if (opts.body) updates.body = opts.body;
|
|
953
|
+
if (Object.keys(updates).length === 0) {
|
|
954
|
+
console.error(opts.json ? JSON.stringify({ error: "Nothing to update. Use --title or --body.", code: "no_input" }) : "Nothing to update. Use --title or --body.");
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
const spinner = opts.json ? null : ora12("Updating idea...").start();
|
|
958
|
+
const updated = await updateIdea(config, ideaId, updates);
|
|
959
|
+
spinner?.stop();
|
|
960
|
+
if (opts.json) {
|
|
961
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
console.log(chalk13.green("\u2713") + ` Updated #${updated.id}`);
|
|
965
|
+
if (opts.title) console.log(` Title: ${chalk13.bold(updated.title)}`);
|
|
966
|
+
if (opts.body) console.log(` Body updated (${updated.body?.length ?? 0} chars)`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/commands/keys.ts
|
|
970
|
+
import chalk14 from "chalk";
|
|
971
|
+
import ora13 from "ora";
|
|
972
|
+
async function keysListCommand(opts) {
|
|
973
|
+
const config = await getAuthenticatedConfig();
|
|
974
|
+
const spinner = opts.json ? null : ora13("Loading API keys...").start();
|
|
975
|
+
const { api_keys } = await listApiKeys(config);
|
|
976
|
+
spinner?.stop();
|
|
977
|
+
if (opts.json) {
|
|
978
|
+
console.log(JSON.stringify({ api_keys }, null, 2));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (api_keys.length === 0) {
|
|
982
|
+
console.log(chalk14.dim("No API keys found. Create one with `nrepo keys create <label>`."));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
console.log(chalk14.bold(`${api_keys.length} API key${api_keys.length === 1 ? "" : "s"}
|
|
986
|
+
`));
|
|
987
|
+
for (const key of api_keys) {
|
|
988
|
+
const lastUsed = key.last_used_at ? formatDate(key.last_used_at) : chalk14.dim("never");
|
|
989
|
+
console.log(` ${chalk14.bold(key.label || chalk14.dim("(no label)"))} ${chalk14.dim(key.id)}`);
|
|
990
|
+
console.log(` Created: ${formatDate(key.created_at)} Last used: ${lastUsed}
|
|
991
|
+
`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async function keysCreateCommand(label, opts) {
|
|
995
|
+
const config = await getAuthenticatedConfig();
|
|
996
|
+
const spinner = opts.json ? null : ora13("Creating API key...").start();
|
|
997
|
+
const result = await createApiKey(config, label);
|
|
998
|
+
spinner?.stop();
|
|
999
|
+
if (opts.json) {
|
|
1000
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
console.log(chalk14.green("\u2713") + " API key created\n");
|
|
1004
|
+
console.log(` Label: ${chalk14.bold(result.label)}`);
|
|
1005
|
+
console.log(` Key: ${chalk14.bold(result.key)}`);
|
|
1006
|
+
console.log(`
|
|
1007
|
+
${chalk14.yellow("Save this key now \u2014 it won't be shown again.")}`);
|
|
1008
|
+
}
|
|
1009
|
+
async function keysRevokeCommand(keyId, opts) {
|
|
1010
|
+
const config = await getAuthenticatedConfig();
|
|
1011
|
+
const spinner = opts.json ? null : ora13("Revoking API key...").start();
|
|
1012
|
+
await deleteApiKey(config, keyId);
|
|
1013
|
+
spinner?.stop();
|
|
1014
|
+
if (opts.json) {
|
|
1015
|
+
console.log(JSON.stringify({ success: true, revoked: keyId }));
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
console.log(chalk14.green("\u2713") + ` API key ${chalk14.dim(keyId)} revoked.`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/commands/link.ts
|
|
1022
|
+
import chalk15 from "chalk";
|
|
1023
|
+
import ora14 from "ora";
|
|
1024
|
+
var VALID_TYPES = RELATION_TYPES.filter((t) => t !== "duplicate");
|
|
1025
|
+
async function linkCommand(sourceId, targetId, opts) {
|
|
1026
|
+
const config = await getAuthenticatedConfig();
|
|
1027
|
+
const src = parseInt(sourceId, 10);
|
|
1028
|
+
const tgt = parseInt(targetId, 10);
|
|
1029
|
+
if (isNaN(src) || isNaN(tgt)) {
|
|
1030
|
+
console.error("Invalid idea IDs");
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
const relationType = opts.type ?? "related";
|
|
1034
|
+
if (!VALID_TYPES.includes(relationType)) {
|
|
1035
|
+
console.error(`Invalid type "${relationType}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
const spinner = opts.json ? null : ora14("Creating link...").start();
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await createRelation(config, src, tgt, relationType, opts.note, opts.force);
|
|
1041
|
+
spinner?.stop();
|
|
1042
|
+
if (opts.json) {
|
|
1043
|
+
console.log(JSON.stringify(result.relation, null, 2));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
console.log(chalk15.green("\u2713") + ` Linked #${src} \u2192 #${tgt} (${relationType})`);
|
|
1047
|
+
if (opts.note) {
|
|
1048
|
+
console.log(chalk15.dim(` Note: ${opts.note}`));
|
|
1049
|
+
}
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
spinner?.stop();
|
|
1052
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
1053
|
+
if (opts.json) {
|
|
1054
|
+
console.error(JSON.stringify({ error: err.message, code: "cycle_detected" }));
|
|
1055
|
+
} else {
|
|
1056
|
+
console.error(chalk15.red(err.message));
|
|
1057
|
+
if (!opts.force && (relationType === "supersedes" || relationType === "parent")) {
|
|
1058
|
+
console.error(chalk15.dim(" Use --force to bypass this check."));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
throw err;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function unlinkCommand(sourceId, targetId, opts) {
|
|
1067
|
+
const config = await getAuthenticatedConfig();
|
|
1068
|
+
const src = parseInt(sourceId, 10);
|
|
1069
|
+
const tgt = parseInt(targetId, 10);
|
|
1070
|
+
if (isNaN(src) || isNaN(tgt)) {
|
|
1071
|
+
console.error("Invalid idea IDs");
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
const spinner = opts.json ? null : ora14("Removing link...").start();
|
|
1075
|
+
const relations = await getIdeaRelations(config, src);
|
|
1076
|
+
const match = relations.outgoing.find((r) => r.idea_id === tgt) ?? relations.incoming.find((r) => r.idea_id === tgt);
|
|
1077
|
+
if (!match) {
|
|
1078
|
+
spinner?.stop();
|
|
1079
|
+
if (opts.json) {
|
|
1080
|
+
console.error(JSON.stringify({ error: "No link found between these ideas" }));
|
|
1081
|
+
} else {
|
|
1082
|
+
console.error(`No link found between #${src} and #${tgt}. Run ${chalk15.cyan(`nrepo links ${src}`)} to see existing links.`);
|
|
1083
|
+
}
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
await deleteRelation(config, match.id);
|
|
1087
|
+
spinner?.stop();
|
|
1088
|
+
if (opts.json) {
|
|
1089
|
+
console.log(JSON.stringify({ success: true }));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
console.log(chalk15.green("\u2713") + ` Unlinked #${src} \u2194 #${tgt}`);
|
|
1093
|
+
}
|
|
1094
|
+
async function linksCommand(id, opts) {
|
|
1095
|
+
const config = await getAuthenticatedConfig();
|
|
1096
|
+
const ideaId = parseInt(id, 10);
|
|
1097
|
+
if (isNaN(ideaId)) {
|
|
1098
|
+
console.error("Invalid idea ID");
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
const spinner = opts.json ? null : ora14("Loading links...").start();
|
|
1102
|
+
const [idea, relations] = await Promise.all([
|
|
1103
|
+
getIdea(config, ideaId),
|
|
1104
|
+
getIdeaRelations(config, ideaId)
|
|
1105
|
+
]);
|
|
1106
|
+
spinner?.stop();
|
|
1107
|
+
let { outgoing, incoming } = relations;
|
|
1108
|
+
if (opts.type) {
|
|
1109
|
+
outgoing = outgoing.filter((r) => r.relation_type === opts.type);
|
|
1110
|
+
incoming = incoming.filter((r) => r.relation_type === opts.type);
|
|
1111
|
+
}
|
|
1112
|
+
if (opts.json) {
|
|
1113
|
+
console.log(JSON.stringify({ outgoing, incoming }, null, 2));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
console.log(chalk15.bold(`Links for #${ideaId} "${idea.title}":`));
|
|
1117
|
+
if (outgoing.length === 0 && incoming.length === 0) {
|
|
1118
|
+
console.log(chalk15.dim(" No links"));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const DISPLAY = {
|
|
1122
|
+
blocks: { out: "Blocks", in: "Blocked by", arrow: "\u2192" },
|
|
1123
|
+
inspires: { out: "Inspires", in: "Inspired by", arrow: "\u2192" },
|
|
1124
|
+
supersedes: { out: "Supersedes", in: "Superseded by", arrow: "\u2192" },
|
|
1125
|
+
parent: { out: "Parent of", in: "Child of", arrow: "\u2192" },
|
|
1126
|
+
related: { out: "Related", in: "Related", arrow: "\u2194" },
|
|
1127
|
+
duplicate: { out: "Similar", in: "Similar", arrow: "\u2194" }
|
|
1128
|
+
};
|
|
1129
|
+
const outByType = /* @__PURE__ */ new Map();
|
|
1130
|
+
for (const r of outgoing) {
|
|
1131
|
+
const list = outByType.get(r.relation_type) ?? [];
|
|
1132
|
+
list.push(r);
|
|
1133
|
+
outByType.set(r.relation_type, list);
|
|
1134
|
+
}
|
|
1135
|
+
const inByType = /* @__PURE__ */ new Map();
|
|
1136
|
+
for (const r of incoming) {
|
|
1137
|
+
const list = inByType.get(r.relation_type) ?? [];
|
|
1138
|
+
list.push(r);
|
|
1139
|
+
inByType.set(r.relation_type, list);
|
|
1140
|
+
}
|
|
1141
|
+
for (const [type, items] of outByType) {
|
|
1142
|
+
const d = DISPLAY[type] ?? { out: type, arrow: "\u2192" };
|
|
1143
|
+
console.log("");
|
|
1144
|
+
console.log(` ${chalk15.bold(d.out)} ${d.arrow}`);
|
|
1145
|
+
for (const r of items) {
|
|
1146
|
+
const status = chalk15.dim(`[${r.idea_status}]`);
|
|
1147
|
+
const note = r.note ? chalk15.dim(` \u2014 ${r.note}`) : "";
|
|
1148
|
+
console.log(` #${r.idea_id} ${r.idea_title} ${status}${note}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
for (const [type, items] of inByType) {
|
|
1152
|
+
if ((type === "related" || type === "duplicate") && outByType.has(type)) continue;
|
|
1153
|
+
const d = DISPLAY[type] ?? { in: type, arrow: "\u2190" };
|
|
1154
|
+
console.log("");
|
|
1155
|
+
console.log(` ${chalk15.bold(d.in)} \u2190`);
|
|
1156
|
+
for (const r of items) {
|
|
1157
|
+
const status = chalk15.dim(`[${r.idea_status}]`);
|
|
1158
|
+
const note = r.note ? chalk15.dim(` \u2014 ${r.note}`) : "";
|
|
1159
|
+
console.log(` #${r.idea_id} ${r.idea_title} ${status}${note}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
console.log("");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/commands/merge.ts
|
|
1166
|
+
import chalk16 from "chalk";
|
|
1167
|
+
import ora15 from "ora";
|
|
1168
|
+
async function mergeCommand(keepId, absorbId, opts) {
|
|
1169
|
+
const config = await getAuthenticatedConfig();
|
|
1170
|
+
const keep = parseInt(keepId, 10);
|
|
1171
|
+
const absorb = parseInt(absorbId, 10);
|
|
1172
|
+
if (isNaN(keep) || isNaN(absorb)) {
|
|
1173
|
+
console.error("Invalid idea IDs");
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
if (keep === absorb) {
|
|
1177
|
+
console.error("Cannot merge an idea with itself");
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
const spinner = opts.json ? null : ora15("Loading ideas...").start();
|
|
1181
|
+
const [keepIdea, absorbIdea] = await Promise.all([
|
|
1182
|
+
getIdea(config, keep),
|
|
1183
|
+
getIdea(config, absorb)
|
|
1184
|
+
]);
|
|
1185
|
+
spinner?.stop();
|
|
1186
|
+
if (!opts.json && !opts.force) {
|
|
1187
|
+
console.log(chalk16.bold("Merge preview:"));
|
|
1188
|
+
console.log(` Keep: #${keep} "${keepIdea.title}" [${keepIdea.status}]`);
|
|
1189
|
+
console.log(` Absorb: #${absorb} "${absorbIdea.title}" [${absorbIdea.status}]`);
|
|
1190
|
+
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."));
|
|
1193
|
+
console.log("");
|
|
1194
|
+
}
|
|
1195
|
+
const mergeSpinner = opts.json ? null : ora15("Merging ideas...").start();
|
|
1196
|
+
const result = await mergeIdeas(config, keep, absorb);
|
|
1197
|
+
mergeSpinner?.stop();
|
|
1198
|
+
if (opts.json) {
|
|
1199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
console.log(chalk16.green("\u2713") + ` Merged #${absorb} into #${keep}`);
|
|
1203
|
+
console.log(` Title: "${result.title}"`);
|
|
1204
|
+
console.log(` Tags: ${result.tags?.length ? result.tags.join(", ") : "none"}`);
|
|
1205
|
+
console.log(` #${absorb} "${absorbIdea.title}" \u2192 shelved (superseded by #${keep})`);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/commands/graph.ts
|
|
1209
|
+
import chalk17 from "chalk";
|
|
1210
|
+
import ora16 from "ora";
|
|
1211
|
+
async function graphCommand(id, opts) {
|
|
1212
|
+
const config = await getAuthenticatedConfig();
|
|
1213
|
+
const startId = parseInt(id, 10);
|
|
1214
|
+
if (isNaN(startId)) {
|
|
1215
|
+
console.error("Invalid idea ID");
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
const maxDepth = Math.min(parseInt(opts.depth ?? "1", 10), 5);
|
|
1219
|
+
const typeFilter = opts.type?.split(",");
|
|
1220
|
+
const spinner = opts.json ? null : ora16("Traversing graph...").start();
|
|
1221
|
+
const visited = /* @__PURE__ */ new Map();
|
|
1222
|
+
const edges = [];
|
|
1223
|
+
const children = /* @__PURE__ */ new Map();
|
|
1224
|
+
let currentLevel = [startId];
|
|
1225
|
+
let depth = 0;
|
|
1226
|
+
const rootIdea = await getIdea(config, startId);
|
|
1227
|
+
visited.set(startId, { id: startId, title: rootIdea.title, status: rootIdea.status, depth: 0 });
|
|
1228
|
+
while (depth < maxDepth && currentLevel.length > 0) {
|
|
1229
|
+
const nextLevel = [];
|
|
1230
|
+
for (const nodeId of currentLevel) {
|
|
1231
|
+
const relations = await getIdeaRelations(config, nodeId);
|
|
1232
|
+
const allRelations = [
|
|
1233
|
+
...relations.outgoing.map((r) => ({ ...r, direction: "out" })),
|
|
1234
|
+
...relations.incoming.map((r) => ({ ...r, direction: "in" }))
|
|
1235
|
+
];
|
|
1236
|
+
for (const rel of allRelations) {
|
|
1237
|
+
if (typeFilter && !typeFilter.includes(rel.relation_type)) continue;
|
|
1238
|
+
const neighborId = rel.idea_id;
|
|
1239
|
+
if (visited.has(neighborId)) continue;
|
|
1240
|
+
visited.set(neighborId, {
|
|
1241
|
+
id: neighborId,
|
|
1242
|
+
title: rel.idea_title,
|
|
1243
|
+
status: rel.idea_status,
|
|
1244
|
+
depth: depth + 1
|
|
1245
|
+
});
|
|
1246
|
+
const edgeSource = rel.direction === "out" ? nodeId : neighborId;
|
|
1247
|
+
const edgeTarget = rel.direction === "out" ? neighborId : nodeId;
|
|
1248
|
+
edges.push({ source: edgeSource, target: edgeTarget, type: rel.relation_type });
|
|
1249
|
+
const list = children.get(nodeId) ?? [];
|
|
1250
|
+
list.push({ childId: neighborId, type: rel.relation_type, title: rel.idea_title, status: rel.idea_status });
|
|
1251
|
+
children.set(nodeId, list);
|
|
1252
|
+
nextLevel.push(neighborId);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
currentLevel = nextLevel;
|
|
1256
|
+
depth++;
|
|
1257
|
+
}
|
|
1258
|
+
spinner?.stop();
|
|
1259
|
+
if (opts.json) {
|
|
1260
|
+
console.log(JSON.stringify({
|
|
1261
|
+
root: startId,
|
|
1262
|
+
depth: maxDepth,
|
|
1263
|
+
types: typeFilter ?? null,
|
|
1264
|
+
nodes: Array.from(visited.values()),
|
|
1265
|
+
edges
|
|
1266
|
+
}, null, 2));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const statusStyle3 = {
|
|
1270
|
+
captured: chalk17.gray,
|
|
1271
|
+
exploring: chalk17.cyan,
|
|
1272
|
+
building: chalk17.yellow,
|
|
1273
|
+
shipped: chalk17.green,
|
|
1274
|
+
shelved: chalk17.dim
|
|
1275
|
+
};
|
|
1276
|
+
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
|
|
1283
|
+
};
|
|
1284
|
+
function renderTree(nodeId, prefix, isLast, isRoot) {
|
|
1285
|
+
const node = visited.get(nodeId);
|
|
1286
|
+
const style = statusStyle3[node.status] ?? chalk17.white;
|
|
1287
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1288
|
+
const childPrefix = isRoot ? "" : isLast ? " " : "\u2502 ";
|
|
1289
|
+
const nodeChildren = children.get(nodeId) ?? [];
|
|
1290
|
+
if (isRoot) {
|
|
1291
|
+
console.log(`#${node.id} ${node.title} ${style(`[${node.status}]`)}`);
|
|
1292
|
+
} else {
|
|
1293
|
+
const parentRel = nodeChildren.length > 0 ? "" : "";
|
|
1294
|
+
console.log(`${prefix}${connector}#${node.id} ${node.title} ${style(`[${node.status}]`)}`);
|
|
1295
|
+
}
|
|
1296
|
+
for (const [i, child] of nodeChildren.entries()) {
|
|
1297
|
+
const last = i === nodeChildren.length - 1;
|
|
1298
|
+
const tc = typeColor[child.type] ?? chalk17.white;
|
|
1299
|
+
const childNode = visited.get(child.childId);
|
|
1300
|
+
const childStyle = statusStyle3[childNode.status] ?? chalk17.white;
|
|
1301
|
+
const childConnector = last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1302
|
+
const nextPrefix = prefix + childPrefix + (last ? " " : "\u2502 ");
|
|
1303
|
+
console.log(
|
|
1304
|
+
`${prefix}${childPrefix}${childConnector}${tc(child.type)} \u2192 #${child.childId} ${child.title} ${childStyle(`[${childNode.status}]`)}`
|
|
1305
|
+
);
|
|
1306
|
+
const grandchildren = children.get(child.childId) ?? [];
|
|
1307
|
+
for (const [j, gc] of grandchildren.entries()) {
|
|
1308
|
+
const gcLast = j === grandchildren.length - 1;
|
|
1309
|
+
const gcNode = visited.get(gc.childId);
|
|
1310
|
+
if (!gcNode) continue;
|
|
1311
|
+
const gcStyle = statusStyle3[gcNode.status] ?? chalk17.white;
|
|
1312
|
+
const gcTc = typeColor[gc.type] ?? chalk17.white;
|
|
1313
|
+
const gcConnector = gcLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1314
|
+
console.log(
|
|
1315
|
+
`${nextPrefix}${gcConnector}${gcTc(gc.type)} \u2192 #${gc.childId} ${gc.title} ${gcStyle(`[${gcNode.status}]`)}`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (visited.size === 1) {
|
|
1321
|
+
const root = visited.get(startId);
|
|
1322
|
+
const style = statusStyle3[root.status] ?? chalk17.white;
|
|
1323
|
+
console.log(`#${root.id} ${root.title} ${style(`[${root.status}]`)}`);
|
|
1324
|
+
console.log(chalk17.dim(" No connections found"));
|
|
1325
|
+
} else {
|
|
1326
|
+
renderTree(startId, "", true, true);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/index.ts
|
|
1331
|
+
var program = new Command();
|
|
1332
|
+
program.name("nrepo").description("NeuralRepo \u2014 capture and manage ideas from the terminal").version("0.0.1");
|
|
1333
|
+
program.command("login").description("Authenticate with NeuralRepo").option("--api-key", "Login with an API key instead of browser OAuth").action(wrap(loginCommand));
|
|
1334
|
+
program.command("logout").description("Clear stored credentials").action(wrap(async () => {
|
|
1335
|
+
await clearConfig();
|
|
1336
|
+
console.log("Logged out.");
|
|
1337
|
+
}));
|
|
1338
|
+
program.command("whoami").description("Show current user info").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(whoamiCommand));
|
|
1339
|
+
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
|
+
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
|
+
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
|
+
program.command("status").description("Overview dashboard").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(statusCommand));
|
|
1344
|
+
program.command("show <id>").description("Show full idea detail").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(showCommand));
|
|
1345
|
+
program.command("edit <id>").description("Update an idea's title or body").option("--title <title>", "New title").option("--body <body>", "New body").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(editCommand));
|
|
1346
|
+
program.command("move <id-or-status> [status]").description("Change idea status (single: move <id> <status>, bulk: move <status> --ids 1,2,3)").option("--ids <ids>", "Comma-separated idea IDs for bulk move").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(async (idOrStatus, status, opts) => {
|
|
1347
|
+
if (opts.ids) {
|
|
1348
|
+
await moveBulkCommand(idOrStatus, { ids: opts.ids, json: opts.json });
|
|
1349
|
+
} else if (status) {
|
|
1350
|
+
await moveCommand(idOrStatus, status, { json: opts.json });
|
|
1351
|
+
} else {
|
|
1352
|
+
console.error("Usage: nrepo move <id> <status> or nrepo move <status> --ids 1,2,3");
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
}));
|
|
1356
|
+
var tagCmd = program.command("tag").description("Manage tags (tag <id> <tags...> or tag add/remove <tag> --ids 1,2,3)");
|
|
1357
|
+
tagCmd.command("add <tag>").description("Add tag to multiple ideas").requiredOption("--ids <ids>", "Comma-separated idea IDs").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(tagAddCommand));
|
|
1358
|
+
tagCmd.command("remove <tag>").description("Remove tag from multiple ideas").requiredOption("--ids <ids>", "Comma-separated idea IDs").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(tagRemoveCommand));
|
|
1359
|
+
tagCmd.argument("[id]").argument("[tags...]").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(async (id, tags, opts) => {
|
|
1360
|
+
if (id && tags && tags.length > 0) {
|
|
1361
|
+
await tagCommand(id, tags, { json: opts?.json });
|
|
1362
|
+
}
|
|
1363
|
+
}));
|
|
1364
|
+
program.command("pull <id>").description("Export idea + context as local files").option("--to <dir>", "Output directory (default: current)").option("--json", "Output as JSON (prints idea data instead of writing files)").option("--human", "Force human-readable output").action(wrap(pullCommand));
|
|
1365
|
+
program.command("diff <id> [id2]").description("Compare two ideas side-by-side (or diff against parent/related)").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(diffCommand));
|
|
1366
|
+
program.command("branch <id>").description("Fork an idea into a new variant").option("--title <title>", "Override title for the branch").option("--body <body>", "Override body for the branch").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(branchCommand));
|
|
1367
|
+
program.command("link <source-id> <target-id>").description("Create a link between two ideas").option("--type <type>", "Link type (related|blocks|inspires|supersedes|parent)", "related").option("--note <note>", "Add a note to the link").option("--force", "Bypass cycle detection for soft-block types").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(linkCommand));
|
|
1368
|
+
program.command("unlink <source-id> <target-id>").description("Remove a link between two ideas").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(unlinkCommand));
|
|
1369
|
+
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
|
+
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
|
+
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");
|
|
1373
|
+
keys.command("list").description("List all API keys").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(keysListCommand));
|
|
1374
|
+
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
|
+
keys.command("revoke <key-id>").description("Revoke an API key").option("--json", "Output as JSON").option("--human", "Force human-readable output").action(wrap(keysRevokeCommand));
|
|
1376
|
+
program.parse();
|
|
1377
|
+
function collect(value, previous) {
|
|
1378
|
+
return previous.concat([value]);
|
|
1379
|
+
}
|
|
1380
|
+
function wrap(fn) {
|
|
1381
|
+
return (async (...args) => {
|
|
1382
|
+
let jsonMode = false;
|
|
1383
|
+
if (args.length >= 2) {
|
|
1384
|
+
const opts = args[args.length - 2];
|
|
1385
|
+
if (opts && typeof opts === "object" && !(opts instanceof Command)) {
|
|
1386
|
+
const o = opts;
|
|
1387
|
+
if (o.human) {
|
|
1388
|
+
o.json = false;
|
|
1389
|
+
} else if (!o.json) {
|
|
1390
|
+
const config = await loadConfig();
|
|
1391
|
+
if (config?.auth_method === "api-key") {
|
|
1392
|
+
o.json = true;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
jsonMode = !!o.json;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
await fn(...args);
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
if (jsonMode) {
|
|
1402
|
+
const error = errorToJson(err);
|
|
1403
|
+
console.error(JSON.stringify(error));
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
if (err instanceof AuthError) {
|
|
1407
|
+
console.error(chalk18.red(err.message));
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
if (err instanceof ApiError) {
|
|
1411
|
+
if (err.status === 401) {
|
|
1412
|
+
console.error(chalk18.red("Authentication expired. Run `nrepo login` to re-authenticate."));
|
|
1413
|
+
} else if (err.status === 403) {
|
|
1414
|
+
console.error(chalk18.yellow("This feature requires a Pro plan. Upgrade at https://neuralrepo.com/settings"));
|
|
1415
|
+
} else {
|
|
1416
|
+
console.error(chalk18.red(`API error (${err.status}): ${err.message}`));
|
|
1417
|
+
}
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
if (err instanceof Error && err.message.startsWith("Network error")) {
|
|
1421
|
+
console.error(chalk18.red(err.message));
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
function errorToJson(err) {
|
|
1429
|
+
if (err instanceof AuthError) {
|
|
1430
|
+
return { error: err.message, code: "auth_required" };
|
|
1431
|
+
}
|
|
1432
|
+
if (err instanceof ApiError) {
|
|
1433
|
+
return { error: err.message, code: `http_${err.status}`, status: err.status };
|
|
1434
|
+
}
|
|
1435
|
+
if (err instanceof Error && err.message.startsWith("Network error")) {
|
|
1436
|
+
return { error: err.message, code: "network_error" };
|
|
1437
|
+
}
|
|
1438
|
+
return { error: String(err), code: "unknown" };
|
|
1439
|
+
}
|