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
95 changes: 95 additions & 0 deletions cli/notecode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/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
*/

import { Command } from 'commander';
import { createTaskCommands } from './src/commands/task.js';
import { createSessionCommands } from './src/commands/session.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', '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());

// 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"
}
90 changes: 90 additions & 0 deletions cli/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* 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');
}
118 changes: 118 additions & 0 deletions cli/src/commands/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* NoteCode CLI - Session Commands
*/

import { Command } from 'commander';
import * as api from '../api.js';
import {
formatSessionRow,
formatSessionHeader,
formatSessionDetails,
printError,
} from '../formatters.js';

export function createSessionCommands() {
const session = new Command('session')
.description('Manage sessions');

// session list
session
.command('list')
.description('List sessions')
.option('--task-id <id>', 'Filter by task ID')
.option('--status <status>', 'Filter by status (running, completed, etc.)')
.option('--limit <n>', 'Maximum number of results', '20')
.option('--json', 'Output as JSON')
.action(async (opts) => {
try {
let sessions;

// If filtering by running status, use the dedicated endpoint
if (opts.status === 'running') {
const result = await api.listRunningSessions();
sessions = result.sessions;
} else {
const result = await api.listSessions({
taskId: opts.taskId,
limit: parseInt(opts.limit, 10),
});
sessions = result.sessions;

// Client-side filter by status if specified
if (opts.status && opts.status !== 'running') {
sessions = sessions.filter(s => s.status === opts.status);
}
}

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

if (sessions.length === 0) {
console.log('No sessions found.');
return;
}

console.log(formatSessionHeader());
console.log('-'.repeat(100));
sessions.forEach(s => console.log(formatSessionRow(s)));
console.log(`\n${sessions.length} session(s) found.`);
} catch (err) {
printError(err.message);
process.exit(1);
}
});

// session status <id> (alias for get, more intuitive)
session
.command('status <id>')
.description('Get session status and details')
.option('--json', 'Output as JSON')
.action(async (id, opts) => {
try {
const { session: sessionData } = await api.getSession(id);

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

console.log(formatSessionDetails(sessionData));
} catch (err) {
if (err.status === 404) {
printError(`Session not found: ${id}`);
} else {
printError(err.message);
}
process.exit(1);
}
});

// session get <id> (same as status, for consistency)
session
.command('get <id>')
.description('Get session details (alias for status)')
.option('--json', 'Output as JSON')
.action(async (id, opts) => {
try {
const { session: sessionData } = await api.getSession(id);

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

console.log(formatSessionDetails(sessionData));
} catch (err) {
if (err.status === 404) {
printError(`Session not found: ${id}`);
} else {
printError(err.message);
}
process.exit(1);
}
});

return session;
}
Loading