Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 427 additions & 68 deletions backend/bin/cli.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@fastify/static": "^6.12.0",
"@lancedb/lancedb": "^0.23.0",
"better-sqlite3": "^11.10.0",
"commander": "^14.0.3",
"diff": "^8.0.3",
"drizzle-orm": "^0.30.0",
"fast-glob": "^3.3.3",
Expand Down
107 changes: 107 additions & 0 deletions cli/notecode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env node

/**
* NoteCode CLI
* Command-line interface for managing tasks and sessions
*
* Usage:
* notecode Start the NoteCode server (default)
* notecode serve [-p <port>] Start the NoteCode server
* notecode task list [--status <s>] List tasks
* notecode task get <id> [--json] Get task details
* notecode task create --title "..." Create a task
* notecode task update <id> [options] Update a task
* notecode session list [--task-id <id>] List sessions
* notecode session status <id> Get session details
* notecode approval list [--session <id>] List pending approvals
* notecode approval get <id> Get approval details
* notecode approval approve <id> Approve a request
* notecode approval reject <id> -r Reject a request (reason required)
* notecode watch [--json] Real-time activity monitoring
* notecode status [--json] Show system status summary
*/

import { Command } from 'commander';
import { createTaskCommands } from './src/commands/task.js';
import { createSessionCommands } from './src/commands/session.js';
import { createApprovalCommands } from './src/commands/approval.js';
import { createWatchCommand } from './src/commands/watch.js';
import { createStatusCommand } from './src/commands/status.js';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { spawn } from 'child_process';

// Get version from root package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

let version = '0.1.0';
try {
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
version = pkg.version;
} catch {
// Ignore
}

/**
* Start the NoteCode server using the existing backend CLI
*/
function startServer(args = []) {
const serverCli = join(__dirname, '../backend/bin/cli.js');
const child = spawn('node', [serverCli, ...args], {
stdio: 'inherit',
env: process.env,
});

child.on('exit', (code) => {
process.exit(code ?? 0);
});
}

// Check if we should start the server (no subcommand, or server-related flags)
const args = process.argv.slice(2);
const serverFlags = ['-p', '--port', '--no-browser'];
const subcommands = ['task', 'session', 'approval', 'watch', 'status', 'serve', 'help', '--help', '-h', '--version', '-V'];

// If no args, or only server flags, start the server
const hasSubcommand = args.some(arg => subcommands.includes(arg));
const hasOnlyServerFlags = args.length > 0 && args.every(arg =>
serverFlags.includes(arg) ||
(args.indexOf(arg) > 0 && serverFlags.includes(args[args.indexOf(arg) - 1]))
);

if (args.length === 0 || (hasOnlyServerFlags && !hasSubcommand)) {
startServer(args);
} else {
// Run management CLI
const program = new Command();

program
.name('notecode')
.description('NoteCode CLI - AI Coding Task Management')
.version(version);

// Server command (explicit)
program
.command('serve')
.description('Start the NoteCode server')
.option('-p, --port <port>', 'Server port')
.option('--no-browser', 'Do not open browser automatically')
.action((opts) => {
const serverArgs = [];
if (opts.port) serverArgs.push('-p', opts.port);
if (opts.browser === false) serverArgs.push('--no-browser');
startServer(serverArgs);
});

// Register management subcommands
program.addCommand(createTaskCommands());
program.addCommand(createSessionCommands());
program.addCommand(createApprovalCommands());
program.addCommand(createWatchCommand());
program.addCommand(createStatusCommand());

// Parse and execute
program.parse();
}
3 changes: 3 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
115 changes: 115 additions & 0 deletions cli/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* NoteCode CLI - API Client
* Communicates with the NoteCode REST API
*/

const API_BASE = process.env.NOTECODE_API_URL || 'http://localhost:41920';

/**
* Make an API request
* @param {string} method - HTTP method
* @param {string} path - API path (e.g., '/api/tasks')
* @param {object} body - Request body (for POST/PATCH)
* @returns {Promise<object>} - API response
*/
async function request(method, path, body = null) {
const url = `${API_BASE}${path}`;

const options = {
method,
headers: {
'Content-Type': 'application/json',
},
};

if (body) {
options.body = JSON.stringify(body);
}

try {
const response = await fetch(url, options);
const data = await response.json();

if (!response.ok) {
const error = new Error(data.error || `HTTP ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}

return data;
} catch (err) {
if (err.cause?.code === 'ECONNREFUSED') {
throw new Error('Cannot connect to NoteCode server. Is it running on http://localhost:41920?');
}
throw err;
}
}

// Task API
export async function listTasks(filters = {}) {
const params = new URLSearchParams();
if (filters.status) params.set('status', filters.status);
if (filters.priority) params.set('priority', filters.priority);
if (filters.projectId) params.set('projectId', filters.projectId);
if (filters.agentId) params.set('agentId', filters.agentId);
if (filters.search) params.set('search', filters.search);

const query = params.toString();
return request('GET', `/api/tasks${query ? '?' + query : ''}`);
}

export async function getTask(id) {
return request('GET', `/api/tasks/${id}`);
}

export async function createTask(data) {
return request('POST', '/api/tasks', data);
}

export async function updateTask(id, data) {
return request('PATCH', `/api/tasks/${id}`, data);
}

// Session API
export async function listSessions(filters = {}) {
const params = new URLSearchParams();
if (filters.taskId) params.set('taskId', filters.taskId);
if (filters.limit) params.set('limit', filters.limit.toString());

const query = params.toString();
return request('GET', `/api/sessions${query ? '?' + query : ''}`);
}

export async function getSession(id) {
return request('GET', `/api/sessions/${id}`);
}

export async function listRunningSessions() {
return request('GET', '/api/sessions/running');
}

// Approval API
export async function listPendingApprovals() {
return request('GET', '/api/approvals/pending');
}

export async function getApproval(id) {
return request('GET', `/api/approvals/${id}`);
}

export async function approveApproval(id, message) {
return request('POST', `/api/approvals/${id}/approve`, { decidedBy: message || 'cli' });
}

export async function rejectApproval(id, reason) {
return request('POST', `/api/approvals/${id}/reject`, { decidedBy: reason || 'cli' });
}

export async function listApprovalsBySession(sessionId) {
return request('GET', `/api/approvals/session/${sessionId}`);
}

export async function getApprovalStatus(id) {
return request('GET', `/api/approvals/${id}/status`);
}
117 changes: 117 additions & 0 deletions cli/src/commands/approval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* NoteCode CLI - Approval Commands
* Manage approval requests for tool/diff operations
*/

import { Command } from 'commander';
import {
listPendingApprovals,
getApproval,
approveApproval,
rejectApproval,
listApprovalsBySession,
} from '../api.js';
import {
formatApprovalRow,
formatApprovalHeader,
formatApprovalDetails,
printError,
printSuccess,
} from '../formatters.js';

export function createApprovalCommands() {
const approval = new Command('approval')
.description('Manage approval requests');

// approval list
approval
.command('list')
.description('List pending approvals')
.option('--session <id>', 'Filter by session ID')
.option('--json', 'Output as JSON')
.action(async (opts) => {
try {
let approvals;
if (opts.session) {
const result = await listApprovalsBySession(opts.session);
approvals = result.approvals;
} else {
const result = await listPendingApprovals();
approvals = result.approvals;
}

if (opts.json) {
console.log(JSON.stringify(approvals, null, 2));
return;
}

if (approvals.length === 0) {
console.log('No pending approvals');
return;
}

console.log(formatApprovalHeader());
console.log('-'.repeat(100));
for (const approval of approvals) {
console.log(formatApprovalRow(approval));
}
console.log(`\n${approvals.length} approval(s) pending`);
} catch (err) {
printError(err.message);
process.exit(1);
}
});

// approval get <id>
approval
.command('get <id>')
.description('Get approval details')
.option('--json', 'Output as JSON')
.action(async (id, opts) => {
try {
const result = await getApproval(id);

if (opts.json) {
console.log(JSON.stringify(result, null, 2));
return;
}

console.log(formatApprovalDetails(result.approval, result.diffs));
} catch (err) {
printError(err.message);
process.exit(1);
}
});

// approval approve <id>
approval
.command('approve <id>')
.description('Approve a pending request')
.option('-m, --message <message>', 'Approval message/reason')
.action(async (id, opts) => {
try {
await approveApproval(id, opts.message);
printSuccess(`Approved: ${id}`);
} catch (err) {
printError(err.message);
process.exit(1);
}
});

// approval reject <id>
approval
.command('reject <id>')
.description('Reject a pending request')
.requiredOption('-r, --reason <reason>', 'Rejection reason (required)')
.action(async (id, opts) => {
try {
await rejectApproval(id, opts.reason);
printSuccess(`Rejected: ${id}`);
} catch (err) {
printError(err.message);
process.exit(1);
}
});

return approval;
}
Loading