@shakecodeslikecray/npc-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/adapters/claude-code.d.ts +21 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +83 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/index.d.ts +36 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +50 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/client/api.d.ts +50 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +325 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/auth.d.ts +47 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +129 -0
- package/dist/client/auth.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1055 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +22 -0
- package/dist/mcp.js.map +1 -0
- package/dist/tools/execution.d.ts +3 -0
- package/dist/tools/execution.d.ts.map +1 -0
- package/dist/tools/execution.js +255 -0
- package/dist/tools/execution.js.map +1 -0
- package/dist/tools/ingestion.d.ts +3 -0
- package/dist/tools/ingestion.d.ts.map +1 -0
- package/dist/tools/ingestion.js +131 -0
- package/dist/tools/ingestion.js.map +1 -0
- package/dist/tools/references.d.ts +3 -0
- package/dist/tools/references.d.ts.map +1 -0
- package/dist/tools/references.js +101 -0
- package/dist/tools/references.js.map +1 -0
- package/dist/tools/reporting.d.ts +3 -0
- package/dist/tools/reporting.d.ts.map +1 -0
- package/dist/tools/reporting.js +285 -0
- package/dist/tools/reporting.js.map +1 -0
- package/dist/tools/telemetry.d.ts +3 -0
- package/dist/tools/telemetry.d.ts.map +1 -0
- package/dist/tools/telemetry.js +73 -0
- package/dist/tools/telemetry.js.map +1 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
|
+
import * as auth from "./client/auth.js";
|
|
8
|
+
import * as api from "./client/api.js";
|
|
9
|
+
import { CREDENTIALS_PATH, DEFAULT_API_URL } from "./config.js";
|
|
10
|
+
import { getAdapter, listSupportedTools, DEFAULT_TOOL } from "./adapters/index.js";
|
|
11
|
+
function prompt(question) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function promptPassword(question) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
process.stdout.write(question);
|
|
20
|
+
const stdin = process.stdin;
|
|
21
|
+
stdin.resume();
|
|
22
|
+
if (stdin.isTTY)
|
|
23
|
+
stdin.setRawMode(true);
|
|
24
|
+
stdin.setEncoding("utf8");
|
|
25
|
+
let pw = "";
|
|
26
|
+
const onData = (c) => {
|
|
27
|
+
if (c === "\n" || c === "\r" || c === "\u0004") {
|
|
28
|
+
if (stdin.isTTY)
|
|
29
|
+
stdin.setRawMode(false);
|
|
30
|
+
stdin.removeListener("data", onData);
|
|
31
|
+
stdin.pause();
|
|
32
|
+
process.stdout.write("\n");
|
|
33
|
+
resolve(pw);
|
|
34
|
+
}
|
|
35
|
+
else if (c === "\u007f" || c === "\b") {
|
|
36
|
+
if (pw.length > 0) {
|
|
37
|
+
pw = pw.slice(0, -1);
|
|
38
|
+
process.stdout.write("\b \b");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (c === "\u0003") {
|
|
42
|
+
if (stdin.isTTY)
|
|
43
|
+
stdin.setRawMode(false);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
pw += c;
|
|
48
|
+
process.stdout.write("*");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
stdin.on("data", onData);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const program = new Command();
|
|
55
|
+
program
|
|
56
|
+
.name("npc")
|
|
57
|
+
.description("NPC CLI -- agent-first project orchestration")
|
|
58
|
+
.version("0.1.0");
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Auth commands
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
const authCmd = program.command("auth").description("Authentication");
|
|
63
|
+
authCmd
|
|
64
|
+
.command("login")
|
|
65
|
+
.description("Login to NPC API")
|
|
66
|
+
.option("-e, --email <email>", "Email address")
|
|
67
|
+
.option("-p, --password <password>", "Password (for scripted/non-interactive auth)")
|
|
68
|
+
.option("-t, --tenant <id>", "Tenant UUID (optional)")
|
|
69
|
+
.option("-u, --url <url>", "API URL", DEFAULT_API_URL)
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
try {
|
|
72
|
+
const email = opts.email || await prompt("Email: ");
|
|
73
|
+
const password = opts.password || await promptPassword("Password: ");
|
|
74
|
+
const res = await auth.login(email, password, opts.url, opts.tenant);
|
|
75
|
+
console.log(`Logged in as ${res.user.email} (${res.user.role})`);
|
|
76
|
+
console.log(`Tenant: ${res.user.tenant_id}`);
|
|
77
|
+
if (res.must_reset_password) {
|
|
78
|
+
console.log("WARNING: You must reset your password.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error("Login failed:", err instanceof Error ? err.message : err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
authCmd
|
|
87
|
+
.command("status")
|
|
88
|
+
.description("Show current auth state")
|
|
89
|
+
.action(() => {
|
|
90
|
+
const creds = auth.getCredentials();
|
|
91
|
+
if (!creds) {
|
|
92
|
+
console.log("Not authenticated.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`Email: ${creds.email}`);
|
|
96
|
+
console.log(`API: ${creds.api_url}`);
|
|
97
|
+
console.log(`Tenant: ${creds.tenant_id}`);
|
|
98
|
+
console.log(`Expires: ${creds.expires_at}`);
|
|
99
|
+
console.log(`Credentials: ${CREDENTIALS_PATH}`);
|
|
100
|
+
});
|
|
101
|
+
authCmd
|
|
102
|
+
.command("logout")
|
|
103
|
+
.description("Clear credentials")
|
|
104
|
+
.action(() => {
|
|
105
|
+
auth.logout();
|
|
106
|
+
console.log("Logged out.");
|
|
107
|
+
});
|
|
108
|
+
authCmd
|
|
109
|
+
.command("agent-key <key>")
|
|
110
|
+
.description("Store an agent API key for queue operations")
|
|
111
|
+
.action((key) => {
|
|
112
|
+
try {
|
|
113
|
+
auth.setApiKey(key);
|
|
114
|
+
console.log("Agent API key stored.");
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error("Failed:", err instanceof Error ? err.message : err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Project commands (project groups)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
const projectCmd = program.command("project").description("Project group operations");
|
|
125
|
+
projectCmd
|
|
126
|
+
.command("create")
|
|
127
|
+
.description("Create a new project group")
|
|
128
|
+
.requiredOption("-n, --name <name>", "Project group name")
|
|
129
|
+
.requiredOption("-i, --identifier <id>", "Short identifier")
|
|
130
|
+
.option("-d, --description <desc>", "Description")
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
try {
|
|
133
|
+
const project = await api.createProject({
|
|
134
|
+
name: opts.name,
|
|
135
|
+
identifier: opts.identifier,
|
|
136
|
+
description: opts.description,
|
|
137
|
+
});
|
|
138
|
+
console.log(`Created project group: ${project.name} (${project.id})`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
projectCmd
|
|
146
|
+
.command("list")
|
|
147
|
+
.description("List all project groups")
|
|
148
|
+
.action(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const projects = await api.listProjects();
|
|
151
|
+
if (projects.length === 0) {
|
|
152
|
+
console.log("No project groups found.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const p of projects) {
|
|
156
|
+
console.log(` ${p.identifier.padEnd(8)} ${p.name.padEnd(30)} ${p.id}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
projectCmd
|
|
165
|
+
.command("get <project-id>")
|
|
166
|
+
.description("Get details for a specific project group")
|
|
167
|
+
.action(async (projectId) => {
|
|
168
|
+
try {
|
|
169
|
+
const project = await api.getProject(projectId);
|
|
170
|
+
console.log(`Name: ${project.name}`);
|
|
171
|
+
console.log(`Identifier: ${project.identifier}`);
|
|
172
|
+
console.log(`ID: ${project.id}`);
|
|
173
|
+
console.log(`Tenant: ${project.tenant_id}`);
|
|
174
|
+
if (project.description)
|
|
175
|
+
console.log(`Description: ${project.description}`);
|
|
176
|
+
console.log(`Created: ${project.created_at}`);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
projectCmd
|
|
184
|
+
.command("stats")
|
|
185
|
+
.description("Show stats for all project groups in the tenant")
|
|
186
|
+
.action(async () => {
|
|
187
|
+
try {
|
|
188
|
+
const stats = await api.getProjectStats();
|
|
189
|
+
for (const [id, s] of Object.entries(stats)) {
|
|
190
|
+
console.log(`\nProject: ${id}`);
|
|
191
|
+
console.log(` Repos: ${s.total_repos} | Items: ${s.total_items} | Done: ${s.done} | In Progress: ${s.in_progress}`);
|
|
192
|
+
console.log(` Not Started: ${s.not_started} | Blocked: ${s.blocked}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Repo commands (individual codebases)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
const repoCmd = program.command("repo").description("Repo operations");
|
|
204
|
+
repoCmd
|
|
205
|
+
.command("create")
|
|
206
|
+
.description("Create a new repo")
|
|
207
|
+
.requiredOption("-n, --name <name>", "Repo name")
|
|
208
|
+
.requiredOption("-i, --identifier <id>", "Short identifier")
|
|
209
|
+
.option("-d, --description <desc>", "Description")
|
|
210
|
+
.option("-p, --project <project-id>", "Parent project group UUID")
|
|
211
|
+
.action(async (opts) => {
|
|
212
|
+
try {
|
|
213
|
+
const repo = await api.createRepo({
|
|
214
|
+
name: opts.name,
|
|
215
|
+
identifier: opts.identifier,
|
|
216
|
+
description: opts.description,
|
|
217
|
+
project_id: opts.project,
|
|
218
|
+
});
|
|
219
|
+
console.log(`Created repo: ${repo.name} (${repo.id})`);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
repoCmd
|
|
227
|
+
.command("list")
|
|
228
|
+
.description("List all repos")
|
|
229
|
+
.action(async () => {
|
|
230
|
+
try {
|
|
231
|
+
const repos = await api.listRepos();
|
|
232
|
+
if (repos.length === 0) {
|
|
233
|
+
console.log("No repos found.");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const r of repos) {
|
|
237
|
+
console.log(` ${r.identifier.padEnd(8)} ${r.name.padEnd(30)} ${r.id}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
repoCmd
|
|
246
|
+
.command("get <repo-id>")
|
|
247
|
+
.description("Get details for a specific repo")
|
|
248
|
+
.action(async (repoId) => {
|
|
249
|
+
try {
|
|
250
|
+
const repo = await api.getRepo(repoId);
|
|
251
|
+
console.log(`Name: ${repo.name}`);
|
|
252
|
+
console.log(`Identifier: ${repo.identifier}`);
|
|
253
|
+
console.log(`ID: ${repo.id}`);
|
|
254
|
+
console.log(`Tenant: ${repo.tenant_id}`);
|
|
255
|
+
if (repo.project_id)
|
|
256
|
+
console.log(`Project: ${repo.project_id}`);
|
|
257
|
+
if (repo.description)
|
|
258
|
+
console.log(`Description: ${repo.description}`);
|
|
259
|
+
console.log(`Created: ${repo.created_at}`);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
repoCmd
|
|
267
|
+
.command("stats")
|
|
268
|
+
.description("Show stats for all repos in the tenant")
|
|
269
|
+
.action(async () => {
|
|
270
|
+
try {
|
|
271
|
+
const stats = await api.getRepoStats();
|
|
272
|
+
for (const [id, s] of Object.entries(stats)) {
|
|
273
|
+
const pct = s.total > 0 ? Math.round((s.done / s.total) * 100) : 0;
|
|
274
|
+
console.log(`\nRepo: ${id}`);
|
|
275
|
+
console.log(` Total: ${s.total} | Done: ${s.done} (${pct}%) | In Progress: ${s.in_progress}`);
|
|
276
|
+
console.log(` Blocked: ${s.blocked} | Not Started: ${s.not_started}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Items commands
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
const itemsCmd = program.command("items").description("Work item operations");
|
|
288
|
+
itemsCmd
|
|
289
|
+
.command("list <repo-id>")
|
|
290
|
+
.description("List work items in a repo")
|
|
291
|
+
.option("-s, --state <state>", "Filter by state")
|
|
292
|
+
.option("--priority <priority>", "Filter by priority")
|
|
293
|
+
.option("--parent <id>", "Filter by parent work item UUID")
|
|
294
|
+
.action(async (repoId, opts) => {
|
|
295
|
+
try {
|
|
296
|
+
const params = {};
|
|
297
|
+
if (opts.state)
|
|
298
|
+
params.state = opts.state;
|
|
299
|
+
if (opts.priority)
|
|
300
|
+
params.priority = opts.priority;
|
|
301
|
+
if (opts.parent)
|
|
302
|
+
params.parent_id = opts.parent;
|
|
303
|
+
const items = await api.getWorkItems(repoId, params);
|
|
304
|
+
if (items.length === 0) {
|
|
305
|
+
console.log("No work items found.");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
for (const wi of items) {
|
|
309
|
+
console.log(` [${wi.state.padEnd(12)}] ${wi.title} (${wi.priority}) ${wi.id.slice(0, 8)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
itemsCmd
|
|
318
|
+
.command("get <repo-id> <item-id>")
|
|
319
|
+
.description("Get a specific work item")
|
|
320
|
+
.action(async (repoId, itemId) => {
|
|
321
|
+
try {
|
|
322
|
+
const wi = await api.getWorkItem(repoId, itemId);
|
|
323
|
+
console.log(`Title: ${wi.title}`);
|
|
324
|
+
console.log(`ID: ${wi.id}`);
|
|
325
|
+
console.log(`State: ${wi.state}`);
|
|
326
|
+
console.log(`Priority: ${wi.priority}`);
|
|
327
|
+
if (wi.description)
|
|
328
|
+
console.log(`Description: ${wi.description}`);
|
|
329
|
+
if (wi.estimated_hours)
|
|
330
|
+
console.log(`Estimated: ${wi.estimated_hours}h`);
|
|
331
|
+
if (wi.actual_hours)
|
|
332
|
+
console.log(`Actual: ${wi.actual_hours}h`);
|
|
333
|
+
if (wi.labels?.length)
|
|
334
|
+
console.log(`Labels: ${wi.labels.join(", ")}`);
|
|
335
|
+
if (wi.claimed_by)
|
|
336
|
+
console.log(`Claimed by: ${wi.claimed_by}`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
itemsCmd
|
|
344
|
+
.command("bulk-create <repo-id> <file>")
|
|
345
|
+
.description("Bulk create work items from a JSON file")
|
|
346
|
+
.action(async (repoId, file) => {
|
|
347
|
+
try {
|
|
348
|
+
const raw = readFileSync(file, "utf-8");
|
|
349
|
+
const items = JSON.parse(raw);
|
|
350
|
+
const created = await api.bulkCreateWorkItems(repoId, items);
|
|
351
|
+
console.log(`Created ${created.length} work items.`);
|
|
352
|
+
for (const wi of created) {
|
|
353
|
+
console.log(` [${wi.id.slice(0, 8)}] ${wi.title}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Edge commands
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
const edgeCmd = program.command("edge").description("DAG edge operations");
|
|
365
|
+
edgeCmd
|
|
366
|
+
.command("list <repo-id>")
|
|
367
|
+
.description("List all edges in a repo")
|
|
368
|
+
.action(async (repoId) => {
|
|
369
|
+
try {
|
|
370
|
+
const edges = await api.listEdges(repoId);
|
|
371
|
+
if (edges.length === 0) {
|
|
372
|
+
console.log("No edges found.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
for (const e of edges) {
|
|
376
|
+
console.log(` ${e.from_id.slice(0, 8)} --${e.edge_type}--> ${e.to_id.slice(0, 8)} (${e.id.slice(0, 8)})`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
edgeCmd
|
|
385
|
+
.command("create")
|
|
386
|
+
.description("Create a dependency edge")
|
|
387
|
+
.requiredOption("--repo <id>", "Repo UUID")
|
|
388
|
+
.requiredOption("--from <id>", "Source work item UUID")
|
|
389
|
+
.requiredOption("--to <id>", "Target work item UUID")
|
|
390
|
+
.option("--type <type>", "Edge type: blocks | relates_to", "blocks")
|
|
391
|
+
.action(async (opts) => {
|
|
392
|
+
try {
|
|
393
|
+
const edge = await api.createEdge(opts.repo, {
|
|
394
|
+
from_id: opts.from,
|
|
395
|
+
to_id: opts.to,
|
|
396
|
+
edge_type: opts.type,
|
|
397
|
+
});
|
|
398
|
+
console.log(`Created edge: ${edge.id}`);
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Task commands
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
program
|
|
409
|
+
.command("tasks")
|
|
410
|
+
.description("List my assigned tasks")
|
|
411
|
+
.action(async () => {
|
|
412
|
+
try {
|
|
413
|
+
const items = await api.getMyTasks();
|
|
414
|
+
if (items.length === 0) {
|
|
415
|
+
console.log("No tasks assigned.");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Group by state
|
|
419
|
+
const grouped = {};
|
|
420
|
+
for (const item of items) {
|
|
421
|
+
if (!grouped[item.state])
|
|
422
|
+
grouped[item.state] = [];
|
|
423
|
+
grouped[item.state].push(item);
|
|
424
|
+
}
|
|
425
|
+
for (const [state, tasks] of Object.entries(grouped)) {
|
|
426
|
+
console.log(`\n${state} (${tasks.length}):`);
|
|
427
|
+
for (const t of tasks) {
|
|
428
|
+
console.log(` [${t.id.slice(0, 8)}] ${t.title} (${t.priority})`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
program
|
|
438
|
+
.command("pull")
|
|
439
|
+
.description("Get next unblocked task from the queue")
|
|
440
|
+
.option("-r, --repo <id>", "Filter by repo UUID")
|
|
441
|
+
.action(async (opts) => {
|
|
442
|
+
try {
|
|
443
|
+
const result = await api.queueNext(opts.repo);
|
|
444
|
+
const wi = result.work_item;
|
|
445
|
+
console.log(`Task: ${wi.title}`);
|
|
446
|
+
console.log(`ID: ${wi.id}`);
|
|
447
|
+
console.log(`State: ${wi.state}`);
|
|
448
|
+
console.log(`Priority: ${wi.priority}`);
|
|
449
|
+
if (wi.description)
|
|
450
|
+
console.log(`Description: ${wi.description}`);
|
|
451
|
+
if (result.ancestors.length > 0) {
|
|
452
|
+
console.log(`Context: ${result.ancestors.map((a) => a.title).join(" > ")}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
program
|
|
461
|
+
.command("start <task-id>")
|
|
462
|
+
.description("Start working on a task")
|
|
463
|
+
.action(async (taskId) => {
|
|
464
|
+
try {
|
|
465
|
+
const wi = await api.startWorkItem(taskId);
|
|
466
|
+
console.log(`Started: ${wi.title} (${wi.state})`);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
program
|
|
474
|
+
.command("complete <task-id>")
|
|
475
|
+
.description("Mark a task as in_review")
|
|
476
|
+
.option("-s, --summary <text>", "Completion summary", "")
|
|
477
|
+
.option("-h, --hours <n>", "Actual hours spent")
|
|
478
|
+
.action(async (taskId, opts) => {
|
|
479
|
+
try {
|
|
480
|
+
const wi = await api.completeWorkItem(taskId, {
|
|
481
|
+
notes: opts.summary,
|
|
482
|
+
actual_hours: opts.hours ? parseFloat(opts.hours) : undefined,
|
|
483
|
+
});
|
|
484
|
+
console.log(`Completed: ${wi.title} (${wi.state})`);
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
program
|
|
492
|
+
.command("block <task-id>")
|
|
493
|
+
.description("Block a task")
|
|
494
|
+
.requiredOption("-r, --reason <text>", "Block reason")
|
|
495
|
+
.action(async (taskId, opts) => {
|
|
496
|
+
try {
|
|
497
|
+
const wi = await api.blockWorkItem(taskId, { reason: opts.reason });
|
|
498
|
+
console.log(`Blocked: ${wi.title}`);
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Signal commands
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
const signalCmd = program.command("signal").description("Signal operations");
|
|
509
|
+
signalCmd
|
|
510
|
+
.command("create <task-id>")
|
|
511
|
+
.description("Report a signal for a task")
|
|
512
|
+
.requiredOption("-c, --category <cat>", "Signal category: process, quality, scope")
|
|
513
|
+
.requiredOption("-t, --type <type>", "Signal type: tests_passed, lint_clean, build_ok, etc.")
|
|
514
|
+
.requiredOption("-v, --value <json>", "Signal value as JSON")
|
|
515
|
+
.action(async (taskId, opts) => {
|
|
516
|
+
try {
|
|
517
|
+
const value = JSON.parse(opts.value);
|
|
518
|
+
const signal = await api.createSignal(taskId, {
|
|
519
|
+
category: opts.category,
|
|
520
|
+
signal_type: opts.type,
|
|
521
|
+
value,
|
|
522
|
+
});
|
|
523
|
+
console.log(`Signal created: ${signal.id}`);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
signalCmd
|
|
531
|
+
.command("list <task-id>")
|
|
532
|
+
.description("List all signals for a task")
|
|
533
|
+
.action(async (taskId) => {
|
|
534
|
+
try {
|
|
535
|
+
const signals = await api.listSignals(taskId);
|
|
536
|
+
if (signals.length === 0) {
|
|
537
|
+
console.log("No signals found.");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
for (const s of signals) {
|
|
541
|
+
console.log(` [${s.category}] ${s.signal_type}: ${JSON.stringify(s.value)} (${s.created_at})`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
signalCmd
|
|
550
|
+
.command("health <task-id>")
|
|
551
|
+
.description("Check signal health for a task")
|
|
552
|
+
.action(async (taskId) => {
|
|
553
|
+
try {
|
|
554
|
+
const health = await api.getSignalHealth(taskId);
|
|
555
|
+
console.log(`Status: ${health.all_green ? "ALL GREEN" : "MISSING SIGNALS"}`);
|
|
556
|
+
console.log(`Required: ${health.required_signals.join(", ") || "none"}`);
|
|
557
|
+
console.log(`Present: ${health.signals.map((s) => s.signal_type).join(", ") || "none"}`);
|
|
558
|
+
if (health.missing.length > 0) {
|
|
559
|
+
console.log(`Missing: ${health.missing.join(", ")}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
program
|
|
568
|
+
.command("transition <task-id> <state>")
|
|
569
|
+
.description("Transition a task to a specific state")
|
|
570
|
+
.action(async (taskId, state) => {
|
|
571
|
+
try {
|
|
572
|
+
const wi = await api.transitionWorkItem(taskId, state);
|
|
573
|
+
console.log(`Transitioned: ${wi.title} -> ${wi.state}`);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
program
|
|
581
|
+
.command("context <module-id>")
|
|
582
|
+
.description("Get full context for a module/work item")
|
|
583
|
+
.action(async (moduleId) => {
|
|
584
|
+
try {
|
|
585
|
+
const ctx = await api.getWorkItemContext(moduleId);
|
|
586
|
+
const wi = ctx.work_item;
|
|
587
|
+
console.log(`\nModule: ${wi.title} (${wi.state})`);
|
|
588
|
+
console.log(`ID: ${wi.id}`);
|
|
589
|
+
if (wi.description)
|
|
590
|
+
console.log(`Description: ${wi.description}`);
|
|
591
|
+
if (ctx.ancestors.length > 0) {
|
|
592
|
+
console.log(`\nAncestors:`);
|
|
593
|
+
for (const a of ctx.ancestors) {
|
|
594
|
+
console.log(` ${a.title} (${a.state})`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (ctx.children.length > 0) {
|
|
598
|
+
console.log(`\nChildren (${ctx.children.length}):`);
|
|
599
|
+
for (const c of ctx.children) {
|
|
600
|
+
console.log(` [${c.state.padEnd(12)}] ${c.title} (${c.id.slice(0, 8)})`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (ctx.blockers.length > 0) {
|
|
604
|
+
console.log(`\nBlockers:`);
|
|
605
|
+
for (const b of ctx.blockers) {
|
|
606
|
+
console.log(` ${b.title} (${b.state})`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Models commands
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
const modelsCmd = program.command("models").description("AI model operations");
|
|
619
|
+
modelsCmd
|
|
620
|
+
.command("list")
|
|
621
|
+
.description("List all AI models (global registry)")
|
|
622
|
+
.action(async () => {
|
|
623
|
+
try {
|
|
624
|
+
const models = await api.listAIModels();
|
|
625
|
+
if (models.length === 0) {
|
|
626
|
+
console.log("No AI models found.");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// Group by provider
|
|
630
|
+
const grouped = {};
|
|
631
|
+
for (const m of models) {
|
|
632
|
+
if (!grouped[m.provider])
|
|
633
|
+
grouped[m.provider] = [];
|
|
634
|
+
grouped[m.provider].push(m);
|
|
635
|
+
}
|
|
636
|
+
for (const [provider, providerModels] of Object.entries(grouped)) {
|
|
637
|
+
console.log(`\n${provider}:`);
|
|
638
|
+
for (const m of providerModels) {
|
|
639
|
+
const ctx = m.context_window ? `${(m.context_window / 1000).toFixed(0)}k` : "-";
|
|
640
|
+
console.log(` ${m.model_name.padEnd(30)} ${m.model_id.padEnd(30)} T${m.capability_tier} ${ctx.padStart(6)}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
modelsCmd
|
|
650
|
+
.command("available")
|
|
651
|
+
.description("List org's available models")
|
|
652
|
+
.action(async () => {
|
|
653
|
+
try {
|
|
654
|
+
const models = await api.listAvailableModels();
|
|
655
|
+
if (models.length === 0) {
|
|
656
|
+
console.log("No available models found.");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
console.log("Available models:");
|
|
660
|
+
for (const m of models) {
|
|
661
|
+
const ctx = m.context_window ? `${(m.context_window / 1000).toFixed(0)}k` : "-";
|
|
662
|
+
console.log(` ${m.model_name.padEnd(30)} ${m.model_id.padEnd(30)} T${m.capability_tier} ${ctx.padStart(6)}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
modelsCmd
|
|
671
|
+
.command("mine")
|
|
672
|
+
.description("List my models")
|
|
673
|
+
.action(async () => {
|
|
674
|
+
try {
|
|
675
|
+
const models = await api.getMyModels();
|
|
676
|
+
if (models.length === 0) {
|
|
677
|
+
console.log("No models set. Use `npc models set` to configure.");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
console.log("My models:");
|
|
681
|
+
for (const m of models) {
|
|
682
|
+
const ctx = m.context_window ? `${(m.context_window / 1000).toFixed(0)}k` : "-";
|
|
683
|
+
console.log(` ${m.model_name.padEnd(30)} ${m.model_id.padEnd(30)} T${m.capability_tier} ${ctx.padStart(6)}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
modelsCmd
|
|
692
|
+
.command("set")
|
|
693
|
+
.description("Set my models")
|
|
694
|
+
.option("-m, --models <ids>", "Comma-separated model_id strings (e.g. claude-opus-4-6,claude-sonnet-4-6)")
|
|
695
|
+
.action(async (opts) => {
|
|
696
|
+
try {
|
|
697
|
+
const modelIdStrings = opts.models
|
|
698
|
+
? opts.models.split(",").map((s) => s.trim())
|
|
699
|
+
: [];
|
|
700
|
+
if (modelIdStrings.length === 0) {
|
|
701
|
+
console.error("Provide model IDs via --models flag.");
|
|
702
|
+
console.error("Example: npc models set --models claude-opus-4-6,claude-sonnet-4-6");
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
// Resolve model_id strings to UUIDs
|
|
706
|
+
const allModels = await api.listAIModels();
|
|
707
|
+
const uuids = [];
|
|
708
|
+
const notFound = [];
|
|
709
|
+
for (const mid of modelIdStrings) {
|
|
710
|
+
const found = allModels.find((m) => m.model_id === mid);
|
|
711
|
+
if (found) {
|
|
712
|
+
uuids.push(found.id);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
notFound.push(mid);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (notFound.length > 0) {
|
|
719
|
+
console.error(`Unknown model IDs: ${notFound.join(", ")}`);
|
|
720
|
+
console.error("Run `npc models list` to see valid model IDs.");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
await api.setMyModels(uuids);
|
|
724
|
+
console.log("Updated your models. Org pool synced.");
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
// Telemetry commands
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
const telemetryCmd = program.command("telemetry").description("Telemetry reporting");
|
|
735
|
+
telemetryCmd
|
|
736
|
+
.command("report-session")
|
|
737
|
+
.description("Parse an agent transcript and report telemetry to NPC")
|
|
738
|
+
.option("--transcript <path>", "Path to transcript JSONL file")
|
|
739
|
+
.option("--work-item <id>", "Work item UUID (overrides auto-detection)")
|
|
740
|
+
.option("--tool <name>", `Agent tool that produced the transcript. Supported parsers: claude_code`, "claude_code")
|
|
741
|
+
.action(async (opts) => {
|
|
742
|
+
try {
|
|
743
|
+
// Only claude_code transcript format is parsed today. Other tools can be
|
|
744
|
+
// added by implementing a parser in src/adapters/<tool>.ts and wiring it
|
|
745
|
+
// in below.
|
|
746
|
+
const supportedParsers = ["claude_code"];
|
|
747
|
+
if (!supportedParsers.includes(opts.tool)) {
|
|
748
|
+
console.log(`Transcript parsing not yet supported for ${opts.tool}. Use npc_report_telemetry MCP tool for manual reporting.`);
|
|
749
|
+
process.exit(0);
|
|
750
|
+
}
|
|
751
|
+
// Read hook input from stdin if no --transcript provided
|
|
752
|
+
let transcriptPath = opts.transcript;
|
|
753
|
+
if (!transcriptPath) {
|
|
754
|
+
// Claude Code Stop hook passes JSON on stdin with transcript_path
|
|
755
|
+
const chunks = [];
|
|
756
|
+
for await (const chunk of process.stdin) {
|
|
757
|
+
chunks.push(chunk);
|
|
758
|
+
}
|
|
759
|
+
const stdinData = Buffer.concat(chunks).toString("utf-8").trim();
|
|
760
|
+
if (stdinData) {
|
|
761
|
+
try {
|
|
762
|
+
const hookInput = JSON.parse(stdinData);
|
|
763
|
+
transcriptPath = hookInput.transcript_path;
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// Not JSON, maybe it's the path itself
|
|
767
|
+
transcriptPath = stdinData;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!transcriptPath) {
|
|
772
|
+
console.error("No transcript path provided. Use --transcript or pipe hook input on stdin.");
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
const { parseClaudeCodeTranscript } = await import("./adapters/claude-code.js");
|
|
776
|
+
const parsed = parseClaudeCodeTranscript(transcriptPath);
|
|
777
|
+
const workItemId = opts.workItem ?? parsed.work_item_id;
|
|
778
|
+
if (!workItemId) {
|
|
779
|
+
console.error("No active work item found in transcript. Use --work-item to specify.");
|
|
780
|
+
process.exit(0); // Exit 0 — not an error, just nothing to report
|
|
781
|
+
}
|
|
782
|
+
const ev = await api.createTelemetry(workItemId, {
|
|
783
|
+
session_id: parsed.session_id,
|
|
784
|
+
agent_tool: opts.tool,
|
|
785
|
+
model: parsed.model,
|
|
786
|
+
tokens_in: parsed.tokens_in,
|
|
787
|
+
tokens_out: parsed.tokens_out,
|
|
788
|
+
cache_read_tokens: parsed.cache_read_tokens,
|
|
789
|
+
cache_creation_tokens: parsed.cache_creation_tokens,
|
|
790
|
+
cost_usd: parsed.cost_usd,
|
|
791
|
+
duration_ms: parsed.duration_ms,
|
|
792
|
+
num_turns: parsed.num_turns,
|
|
793
|
+
tool_calls: parsed.tool_calls,
|
|
794
|
+
tool_failures: parsed.tool_failures,
|
|
795
|
+
metadata: parsed.metadata,
|
|
796
|
+
});
|
|
797
|
+
console.log(`Telemetry reported for work item ${workItemId.slice(0, 8)}: $${ev.cost_usd.toFixed(4)}, ${ev.tokens_in + ev.tokens_out} tokens`);
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
801
|
+
console.error(`Failed to report telemetry: ${msg}`);
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
// Docs commands
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
const docsCmd = program.command("docs").description("Document storage operations");
|
|
809
|
+
docsCmd
|
|
810
|
+
.command("list <repo-id>")
|
|
811
|
+
.description("List all documents stored for a repo")
|
|
812
|
+
.action(async (repoId) => {
|
|
813
|
+
try {
|
|
814
|
+
const docs = await api.listDocuments(repoId);
|
|
815
|
+
if (docs.length === 0) {
|
|
816
|
+
console.log("No documents found.");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
for (const d of docs) {
|
|
820
|
+
const kb = (d.size_bytes / 1024).toFixed(1);
|
|
821
|
+
console.log(` ${d.filename.padEnd(45)} ${kb.padStart(8)}KB ${d.id}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
docsCmd
|
|
830
|
+
.command("upload <repo-id> <file>")
|
|
831
|
+
.description("Upload a document file to a repo")
|
|
832
|
+
.option("-n, --name <filename>", "Override filename (default: basename of <file>)")
|
|
833
|
+
.action(async (repoId, file, opts) => {
|
|
834
|
+
try {
|
|
835
|
+
const content = readFileSync(file);
|
|
836
|
+
const filename = opts.name ?? basename(file);
|
|
837
|
+
const contentType = filename.endsWith(".md") ? "text/markdown" : "text/plain";
|
|
838
|
+
const doc = await api.uploadDocument(repoId, filename, content, contentType);
|
|
839
|
+
const baseUrl = auth.getApiUrl().replace(/\/$/, "");
|
|
840
|
+
const contentUrl = `${baseUrl}/api/v1/repos/${repoId}/documents/${doc.id}/content`;
|
|
841
|
+
console.log(`Uploaded: ${doc.filename} (${(doc.size_bytes / 1024).toFixed(1)}KB)`);
|
|
842
|
+
console.log(`ID: ${doc.id}`);
|
|
843
|
+
console.log(`URL: ${contentUrl}`);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
docsCmd
|
|
851
|
+
.command("cat <repo-id> <doc-id>")
|
|
852
|
+
.description("Print document content to stdout")
|
|
853
|
+
.action(async (repoId, docId) => {
|
|
854
|
+
try {
|
|
855
|
+
const content = await api.getDocumentContent(repoId, docId);
|
|
856
|
+
process.stdout.write(content);
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
docsCmd
|
|
864
|
+
.command("delete <repo-id> <doc-id>")
|
|
865
|
+
.description("Delete a document from a repo")
|
|
866
|
+
.action(async (repoId, docId) => {
|
|
867
|
+
try {
|
|
868
|
+
await api.deleteDocument(repoId, docId);
|
|
869
|
+
console.log("Deleted.");
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
docsCmd
|
|
877
|
+
.command("upload-and-link <repo-id> <file> <work-item-id>")
|
|
878
|
+
.description("Upload a document and immediately attach it as a reference to a work item")
|
|
879
|
+
.option("-n, --name <filename>", "Override filename")
|
|
880
|
+
.option("-l, --label <label>", "Reference label (default: filename)")
|
|
881
|
+
.option("-t, --type <ref-type>", "Reference type: prd, design, screenshot, client_doc, external, user_flow", "user_flow")
|
|
882
|
+
.action(async (repoId, file, workItemId, opts) => {
|
|
883
|
+
try {
|
|
884
|
+
const content = readFileSync(file);
|
|
885
|
+
const filename = opts.name ?? basename(file);
|
|
886
|
+
const contentType = filename.endsWith(".md") ? "text/markdown" : "text/plain";
|
|
887
|
+
// 1. Upload
|
|
888
|
+
const doc = await api.uploadDocument(repoId, filename, content, contentType);
|
|
889
|
+
const baseUrl = auth.getApiUrl().replace(/\/$/, "");
|
|
890
|
+
const contentUrl = `${baseUrl}/api/v1/repos/${repoId}/documents/${doc.id}/content`;
|
|
891
|
+
console.log(`Uploaded: ${doc.filename} (${(doc.size_bytes / 1024).toFixed(1)}KB) → ${doc.id}`);
|
|
892
|
+
// 2. Attach as reference
|
|
893
|
+
const label = opts.label ?? filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ");
|
|
894
|
+
const ref = await api.createReference(workItemId, {
|
|
895
|
+
url: contentUrl,
|
|
896
|
+
label,
|
|
897
|
+
ref_type: opts.type,
|
|
898
|
+
});
|
|
899
|
+
console.log(`Linked: [${ref.ref_type}] "${ref.label}" → ${workItemId.slice(0, 8)}`);
|
|
900
|
+
console.log(`Ref ID: ${ref.id}`);
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
// ---------------------------------------------------------------------------
|
|
908
|
+
// Refs commands
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
const refsCmd = program.command("refs").description("Work item reference operations");
|
|
911
|
+
refsCmd
|
|
912
|
+
.command("list <work-item-id>")
|
|
913
|
+
.description("List references for a work item")
|
|
914
|
+
.option("--inherited", "Include references from all ancestor items", false)
|
|
915
|
+
.action(async (workItemId, opts) => {
|
|
916
|
+
try {
|
|
917
|
+
const refs = await api.listReferences(workItemId, opts.inherited);
|
|
918
|
+
if (refs.length === 0) {
|
|
919
|
+
console.log("No references found.");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
for (const r of refs) {
|
|
923
|
+
const from = r.from_level ? ` (from ${r.from_level})` : "";
|
|
924
|
+
console.log(` [${r.ref_type.padEnd(12)}] ${r.label}${from}`);
|
|
925
|
+
console.log(` ${r.url}`);
|
|
926
|
+
console.log(` id: ${r.id}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
refsCmd
|
|
935
|
+
.command("add <work-item-id>")
|
|
936
|
+
.description("Attach a URL reference to a work item")
|
|
937
|
+
.requiredOption("-u, --url <url>", "Reference URL")
|
|
938
|
+
.requiredOption("-l, --label <label>", "Human-readable label")
|
|
939
|
+
.option("-t, --type <type>", "Reference type: prd, design, screenshot, client_doc, external, user_flow", "external")
|
|
940
|
+
.action(async (workItemId, opts) => {
|
|
941
|
+
try {
|
|
942
|
+
const ref = await api.createReference(workItemId, {
|
|
943
|
+
url: opts.url,
|
|
944
|
+
label: opts.label,
|
|
945
|
+
ref_type: opts.type,
|
|
946
|
+
});
|
|
947
|
+
console.log(`Added reference: [${ref.ref_type}] ${ref.label}`);
|
|
948
|
+
console.log(`ID: ${ref.id}`);
|
|
949
|
+
}
|
|
950
|
+
catch (err) {
|
|
951
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
refsCmd
|
|
956
|
+
.command("delete <work-item-id> <ref-id>")
|
|
957
|
+
.description("Delete a reference from a work item")
|
|
958
|
+
.action(async (workItemId, refId) => {
|
|
959
|
+
try {
|
|
960
|
+
await api.deleteReference(workItemId, refId);
|
|
961
|
+
console.log("Deleted.");
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
// MCP server commands
|
|
970
|
+
// ---------------------------------------------------------------------------
|
|
971
|
+
const mcpCmd = program.command("mcp").description("MCP server operations");
|
|
972
|
+
mcpCmd
|
|
973
|
+
.command("start")
|
|
974
|
+
.description("Start MCP server mode (stdio transport)")
|
|
975
|
+
.action(async () => {
|
|
976
|
+
await import("./mcp.js");
|
|
977
|
+
});
|
|
978
|
+
mcpCmd
|
|
979
|
+
.command("install")
|
|
980
|
+
.description("Register NPC as an MCP server in a supported agent tool")
|
|
981
|
+
.option("--tool <name>", `Agent tool to configure. Supported: ${listSupportedTools().join(", ")}`, DEFAULT_TOOL)
|
|
982
|
+
.action((opts) => {
|
|
983
|
+
const adapter = getAdapter(opts.tool);
|
|
984
|
+
if (!adapter) {
|
|
985
|
+
console.error(`Unknown tool: "${opts.tool}". Supported: ${listSupportedTools().join(", ")}`);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
989
|
+
const mcpEntrypoint = join(__dirname, "mcp.js");
|
|
990
|
+
if (!existsSync(mcpEntrypoint)) {
|
|
991
|
+
console.error(`MCP entrypoint not found at ${mcpEntrypoint}`);
|
|
992
|
+
console.error("Run `npm run build` in npc-cli first.");
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
// Read existing config or start fresh
|
|
996
|
+
let config = {};
|
|
997
|
+
if (existsSync(adapter.configPath)) {
|
|
998
|
+
try {
|
|
999
|
+
config = JSON.parse(readFileSync(adapter.configPath, "utf-8"));
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
// Corrupted file — start fresh
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Ensure mcpServers key exists
|
|
1006
|
+
const key = adapter.mcpServersKey;
|
|
1007
|
+
if (!config[key] || typeof config[key] !== "object") {
|
|
1008
|
+
config[key] = {};
|
|
1009
|
+
}
|
|
1010
|
+
config[key].npc = {
|
|
1011
|
+
type: "stdio",
|
|
1012
|
+
command: "node",
|
|
1013
|
+
args: [mcpEntrypoint],
|
|
1014
|
+
};
|
|
1015
|
+
// Create parent dirs if needed
|
|
1016
|
+
mkdirSync(dirname(adapter.configPath), { recursive: true });
|
|
1017
|
+
writeFileSync(adapter.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1018
|
+
console.log(`NPC MCP server registered in ${adapter.displayName}.`);
|
|
1019
|
+
console.log(` Config: ${adapter.configPath}`);
|
|
1020
|
+
console.log(` Entry: node ${mcpEntrypoint}`);
|
|
1021
|
+
console.log(`\n${adapter.restartHint}`);
|
|
1022
|
+
});
|
|
1023
|
+
mcpCmd
|
|
1024
|
+
.command("uninstall")
|
|
1025
|
+
.description("Remove NPC from an agent tool's MCP config")
|
|
1026
|
+
.option("--tool <name>", `Agent tool to remove from. Supported: ${listSupportedTools().join(", ")}`, DEFAULT_TOOL)
|
|
1027
|
+
.action((opts) => {
|
|
1028
|
+
const adapter = getAdapter(opts.tool);
|
|
1029
|
+
if (!adapter) {
|
|
1030
|
+
console.error(`Unknown tool: "${opts.tool}". Supported: ${listSupportedTools().join(", ")}`);
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
if (!existsSync(adapter.configPath)) {
|
|
1034
|
+
console.log(`No ${adapter.displayName} config found. Nothing to remove.`);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const config = JSON.parse(readFileSync(adapter.configPath, "utf-8"));
|
|
1039
|
+
const servers = config[adapter.mcpServersKey];
|
|
1040
|
+
if (!servers?.npc) {
|
|
1041
|
+
console.log(`NPC not found in ${adapter.displayName} MCP config.`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
delete servers.npc;
|
|
1045
|
+
writeFileSync(adapter.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1046
|
+
console.log(`NPC MCP server removed from ${adapter.displayName}.`);
|
|
1047
|
+
console.log(`${adapter.restartHint}`);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
console.error(`Failed to read ${adapter.configPath}`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
program.parse();
|
|
1055
|
+
//# sourceMappingURL=index.js.map
|