diff --git a/cli/src/commands/annotate.zig b/cli/src/commands/annotate.zig deleted file mode 100644 index 0a92a3f..0000000 --- a/cli/src/commands/annotate.zig +++ /dev/null @@ -1,187 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const io = @import("../utils/io.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const manifest = @import("../utils/manifest.zig"); -const pii = @import("../utils/pii.zig"); - -pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { - if (args.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - - if (std.mem.eql(u8, args[0], "--help") or std.mem.eql(u8, args[0], "-h")) { - try printUsage(); - return; - } - - const target = args[0]; - - var author: []const u8 = ""; - var note: ?[]const u8 = null; - var base_override: ?[]const u8 = null; - var json_mode: bool = false; - var privacy_scan: bool = false; - var force: bool = false; - - var i: usize = 1; - while (i < args.len) : (i += 1) { - const a = args[i]; - if (std.mem.eql(u8, a, "--author")) { - if (i + 1 >= args.len) { - colors.printError("Missing value for --author\n", .{}); - return error.InvalidArgs; - } - author = args[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--note")) { - if (i + 1 >= args.len) { - colors.printError("Missing value for --note\n", .{}); - return error.InvalidArgs; - } - note = args[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--base")) { - if (i + 1 >= args.len) { - colors.printError("Missing value for --base\n", .{}); - return error.InvalidArgs; - } - base_override = args[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--json")) { - json_mode = true; - } else if (std.mem.eql(u8, a, "--privacy-scan")) { - privacy_scan = true; - } else if (std.mem.eql(u8, a, "--force")) { - force = true; - } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { - try printUsage(); - return; - } else if (std.mem.startsWith(u8, a, "--")) { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } else { - colors.printError("Unexpected argument: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - if (note == null or std.mem.trim(u8, note.?, " \t\r\n").len == 0) { - colors.printError("--note is required\n", .{}); - try printUsage(); - return error.InvalidArgs; - } - - // PII detection if requested - if (privacy_scan) { - if (pii.scanForPII(note.?, allocator)) |warning| { - colors.printWarning("{s}\n", .{warning.?}); - if (!force) { - colors.printInfo("Use --force to store anyway, or edit your note.\n", .{}); - return error.PIIDetected; - } - } else |_| { - // PII scan failed, continue anyway - } - } - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const resolved_base = base_override orelse cfg.worker_base; - - const manifest_path = manifest.resolvePathWithBase(allocator, target, resolved_base) catch |err| { - if (err == error.FileNotFound) { - colors.printError( - "Could not locate run_manifest.json for '{s}'. Provide a path, or use --base to scan finished/failed/running/pending.\n", - .{target}, - ); - } - return err; - }; - defer allocator.free(manifest_path); - - const job_name = try manifest.readJobNameFromManifest(allocator, manifest_path); - defer allocator.free(job_name); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendAnnotateRun(job_name, author, note.?, api_key_hash); - - if (json_mode) { - const msg = try client.receiveMessage(allocator); - defer allocator.free(msg); - - const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch { - var out = io.stdoutWriter(); - try out.print("{s}\n", .{msg}); - return error.InvalidPacket; - }; - defer packet.deinit(allocator); - - const Result = struct { - ok: bool, - job_name: []const u8, - message: []const u8, - error_code: ?u8 = null, - error_message: ?[]const u8 = null, - details: ?[]const u8 = null, - }; - - var out = io.stdoutWriter(); - if (packet.packet_type == .error_packet) { - const res = Result{ - .ok = false, - .job_name = job_name, - .message = "", - .error_code = @intFromEnum(packet.error_code.?), - .error_message = packet.error_message orelse "", - .details = packet.error_details orelse "", - }; - try out.print("{f}\n", .{std.json.fmt(res, .{})}); - return error.CommandFailed; - } - - const res = Result{ - .ok = true, - .job_name = job_name, - .message = packet.success_message orelse "", - }; - try out.print("{f}\n", .{std.json.fmt(res, .{})}); - return; - } - - try client.receiveAndHandleResponse(allocator, "Annotate"); - - colors.printSuccess("Annotation added\n", .{}); - colors.printInfo("Job: {s}\n", .{job_name}); -} - -fn printUsage() !void { - colors.printInfo("Usage: ml annotate --note [--author ] [--base ] [--json] [--privacy-scan] [--force]\n", .{}); - colors.printInfo("\nOptions:\n", .{}); - colors.printInfo(" --note Annotation text (required)\n", .{}); - colors.printInfo(" --author Author of the annotation\n", .{}); - colors.printInfo(" --base Base path to search for run_manifest.json\n", .{}); - colors.printInfo(" --privacy-scan Scan note for PII before storing\n", .{}); - colors.printInfo(" --force Store even if PII detected\n", .{}); - colors.printInfo(" --json Output JSON response\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml annotate 8b3f... --note \"Try lr=3e-4 next\"\n", .{}); - colors.printInfo(" ml annotate ./finished/job-123 --note \"Baseline looks stable\" --author alice\n", .{}); - colors.printInfo(" ml annotate run_123 --note \"Contact at user@example.com\" --privacy-scan\n", .{}); -} diff --git a/cli/src/commands/experiment.zig b/cli/src/commands/experiment.zig deleted file mode 100644 index 09930e9..0000000 --- a/cli/src/commands/experiment.zig +++ /dev/null @@ -1,882 +0,0 @@ -const std = @import("std"); -const config = @import("../config.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const history = @import("../utils/history.zig"); -const colors = @import("../utils/colors.zig"); -const cancel_cmd = @import("cancel.zig"); -const crypto = @import("../utils/crypto.zig"); -const db = @import("../db.zig"); - -fn jsonError(command: []const u8, message: []const u8) void { - std.debug.print( - "{{\"success\":false,\"command\":\"{s}\",\"error\":\"{s}\"}}\n", - .{ command, message }, - ); -} - -fn jsonErrorWithDetails(command: []const u8, message: []const u8, details: []const u8) void { - std.debug.print( - "{{\"success\":false,\"command\":\"{s}\",\"error\":\"{s}\",\"details\":\"{s}\"}}\n", - .{ command, message, details }, - ); -} - -const ExperimentOptions = struct { - json: bool = false, - help: bool = false, -}; - -pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { - var options = ExperimentOptions{}; - var command_args = std.ArrayList([]const u8).initCapacity(allocator, 10) catch |err| { - return err; - }; - defer command_args.deinit(allocator); - - // Parse flags - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (std.mem.eql(u8, arg, "--json")) { - options.json = true; - } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - options.help = true; - } else { - try command_args.append(allocator, arg); - } - } - - if (command_args.items.len < 1 or options.help) { - try printUsage(); - return; - } - - const command = command_args.items[0]; - - if (std.mem.eql(u8, command, "init")) { - try executeInit(allocator, command_args.items[1..], &options); - } else if (std.mem.eql(u8, command, "create")) { - try executeCreate(allocator, command_args.items[1..], &options); - } else if (std.mem.eql(u8, command, "log")) { - // Route to local or server mode based on config - const cfg = try config.Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - if (cfg.isLocalMode()) { - try executeLogLocal(allocator, command_args.items[1..], &options); - } else { - try executeLog(allocator, command_args.items[1..], &options); - } - } else if (std.mem.eql(u8, command, "show")) { - try executeShow(allocator, command_args.items[1..], &options); - } else if (std.mem.eql(u8, command, "list")) { - // Route to local or server mode based on config - const cfg = try config.Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - if (cfg.isLocalMode()) { - try executeListLocal(allocator, &options); - } else { - try executeList(allocator, &options); - } - } else if (std.mem.eql(u8, command, "delete")) { - if (command_args.items.len < 2) { - if (options.json) { - jsonError("experiment.delete", "Usage: ml experiment delete "); - } else { - colors.printError("Usage: ml experiment delete \n", .{}); - } - return; - } - try executeDelete(allocator, command_args.items[1], &options); - } else { - if (options.json) { - const msg = try std.fmt.allocPrint(allocator, "Unknown command: {s}", .{command}); - defer allocator.free(msg); - jsonError("experiment", msg); - } else { - colors.printError("Unknown command: {s}\n", .{command}); - try printUsage(); - } - } -} - -fn executeInit(allocator: std.mem.Allocator, args: []const []const u8, options: *const ExperimentOptions) !void { - var name: ?[]const u8 = null; - var description: ?[]const u8 = null; - - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (std.mem.eql(u8, arg, "--name")) { - if (i + 1 < args.len) { - name = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--description")) { - if (i + 1 < args.len) { - description = args[i + 1]; - i += 1; - } - } - } - - // Generate experiment ID and commit ID - const stdcrypto = std.crypto; - var exp_id_bytes: [16]u8 = undefined; - stdcrypto.random.bytes(&exp_id_bytes); - - var commit_id_bytes: [20]u8 = undefined; - stdcrypto.random.bytes(&commit_id_bytes); - - const exp_id = try crypto.encodeHexLower(allocator, &exp_id_bytes); - defer allocator.free(exp_id); - - const commit_id = try crypto.encodeHexLower(allocator, &commit_id_bytes); - defer allocator.free(commit_id); - - const exp_name = name orelse "unnamed-experiment"; - const exp_desc = description orelse "No description provided"; - - if (options.json) { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.init\",\"data\":{{\"experiment_id\":\"{s}\",\"commit_id\":\"{s}\",\"name\":\"{s}\",\"description\":\"{s}\",\"status\":\"initialized\"}}}}\n", - .{ exp_id, commit_id, exp_name, exp_desc }, - ); - } else { - colors.printInfo("Experiment initialized successfully!\n", .{}); - colors.printInfo("Experiment ID: {s}\n", .{exp_id}); - colors.printInfo("Commit ID: {s}\n", .{commit_id}); - colors.printInfo("Name: {s}\n", .{exp_name}); - colors.printInfo("Description: {s}\n", .{exp_desc}); - colors.printInfo("Status: initialized\n", .{}); - colors.printInfo("Use this commit ID when queuing jobs: --commit-id {s}\n", .{commit_id}); - } -} - -fn printUsage() !void { - colors.printInfo("Usage: ml experiment [options] [args]\n", .{}); - colors.printInfo("\nOptions:\n", .{}); - colors.printInfo(" --json Output structured JSON\n", .{}); - colors.printInfo(" --help, -h Show this help message\n", .{}); - colors.printInfo("\nCommands:\n", .{}); - colors.printInfo(" init Initialize a new experiment (server mode)\n", .{}); - colors.printInfo(" create --name Create experiment in local mode\n", .{}); - colors.printInfo(" log Log a metric (auto-detects mode)\n", .{}); - colors.printInfo(" show Show experiment details\n", .{}); - colors.printInfo(" list List experiments (auto-detects mode)\n", .{}); - colors.printInfo(" delete Cancel/delete an experiment\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml experiment create --name \"my-experiment\"\n", .{}); - colors.printInfo(" ml experiment show abc123 --json\n", .{}); - colors.printInfo(" ml experiment list --json\n", .{}); -} - -// Local mode implementations -fn executeCreate(allocator: std.mem.Allocator, args: []const []const u8, options: *const ExperimentOptions) !void { - var name: ?[]const u8 = null; - var artifact_path: ?[]const u8 = null; - - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (std.mem.eql(u8, arg, "--name")) { - if (i + 1 < args.len) { - name = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--artifact-path")) { - if (i + 1 < args.len) { - artifact_path = args[i + 1]; - i += 1; - } - } - } - - if (name == null) { - if (options.json) { - jsonError("experiment.create", "--name is required"); - } else { - colors.printError("Error: --name is required\n", .{}); - } - return error.MissingArgument; - } - - // Load config - const cfg = try config.Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - if (!cfg.isLocalMode()) { - if (options.json) { - jsonError("experiment.create", "create only works in local mode (sqlite://)"); - } else { - colors.printError("Error: experiment create only works in local mode (sqlite://)\n", .{}); - } - return error.NotLocalMode; - } - - // Get DB path - const db_path = try cfg.getDBPath(allocator); - defer allocator.free(db_path); - - // Initialize DB - var database = try db.DB.init(allocator, db_path); - defer database.close(); - - // Generate experiment ID - const exp_id = try db.generateUUID(allocator); - defer allocator.free(exp_id); - - // Insert experiment - const sql = "INSERT INTO ml_experiments (experiment_id, name, artifact_path) VALUES (?, ?, ?);"; - const stmt = try database.prepare(sql); - defer db.DB.finalize(stmt); - - try db.DB.bindText(stmt, 1, exp_id); - try db.DB.bindText(stmt, 2, name.?); - try db.DB.bindText(stmt, 3, artifact_path orelse ""); - - _ = try db.DB.step(stmt); - database.checkpointOnExit(); - - if (options.json) { - std.debug.print("{{\"success\":true,\"command\":\"experiment.create\",\"data\":{{\"experiment_id\":\"{s}\",\"name\":\"{s}\"}}}}\n", .{ exp_id, name.? }); - } else { - colors.printSuccess("✓ Created experiment: {s}\n", .{name.?}); - colors.printInfo(" experiment_id: {s}\n", .{exp_id}); - } -} - -fn executeLogLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const ExperimentOptions) !void { - var run_id: ?[]const u8 = null; - var name: ?[]const u8 = null; - var value: ?f64 = null; - var step: i64 = 0; - - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (std.mem.eql(u8, arg, "--run")) { - if (i + 1 < args.len) { - run_id = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--name")) { - if (i + 1 < args.len) { - name = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--value")) { - if (i + 1 < args.len) { - value = std.fmt.parseFloat(f64, args[i + 1]) catch null; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--step")) { - if (i + 1 < args.len) { - step = std.fmt.parseInt(i64, args[i + 1], 10) catch 0; - i += 1; - } - } - } - - if (run_id == null or name == null or value == null) { - if (options.json) { - jsonError("experiment.log", "Usage: ml experiment log --run --name --value [--step ]"); - } else { - colors.printError("Usage: ml experiment log --run --name --value [--step ]\n", .{}); - } - return; - } - - // Load config - const cfg = try config.Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - // Get DB path - const db_path = try cfg.getDBPath(allocator); - defer allocator.free(db_path); - - // Initialize DB - var database = try db.DB.init(allocator, db_path); - defer database.close(); - - // Insert metric - const sql = "INSERT INTO ml_metrics (run_id, key, value, step) VALUES (?, ?, ?, ?);"; - const stmt = try database.prepare(sql); - defer db.DB.finalize(stmt); - - try db.DB.bindText(stmt, 1, run_id.?); - try db.DB.bindText(stmt, 2, name.?); - try db.DB.bindDouble(stmt, 3, value.?); - try db.DB.bindInt64(stmt, 4, step); - - _ = try db.DB.step(stmt); - - if (options.json) { - std.debug.print("{{\"success\":true,\"command\":\"experiment.log\",\"data\":{{\"run_id\":\"{s}\",\"metric\":{{\"name\":\"{s}\",\"value\":{d},\"step\":{d}}}}}}}\n", .{ run_id.?, name.?, value.?, step }); - } else { - colors.printSuccess("✓ Logged metric: {s} = {d:.4} (step {d})\n", .{ name.?, value.?, step }); - } -} - -fn executeListLocal(allocator: std.mem.Allocator, options: *const ExperimentOptions) !void { - // Load config - const cfg = try config.Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - // Get DB path - const db_path = try cfg.getDBPath(allocator); - defer allocator.free(db_path); - - // Initialize DB - var database = try db.DB.init(allocator, db_path); - defer database.close(); - - // Query experiments - const sql = "SELECT experiment_id, name, lifecycle, created_at FROM ml_experiments ORDER BY created_at DESC;"; - const stmt = try database.prepare(sql); - defer db.DB.finalize(stmt); - - var experiments = std.ArrayList(struct { id: []const u8, name: []const u8, lifecycle: []const u8, created: []const u8 }).init(allocator); - defer { - for (experiments.items) |exp| { - allocator.free(exp.id); - allocator.free(exp.name); - allocator.free(exp.lifecycle); - allocator.free(exp.created); - } - experiments.deinit(); - } - - while (try db.DB.step(stmt)) { - const id = try allocator.dupe(u8, db.DB.columnText(stmt, 0)); - const name = try allocator.dupe(u8, db.DB.columnText(stmt, 1)); - const lifecycle = try allocator.dupe(u8, db.DB.columnText(stmt, 2)); - const created = try allocator.dupe(u8, db.DB.columnText(stmt, 3)); - try experiments.append(.{ .id = id, .name = name, .lifecycle = lifecycle, .created = created }); - } - - if (options.json) { - std.debug.print("{{\"success\":true,\"command\":\"experiment.list\",\"data\":{{\"experiments\":[", .{}); - for (experiments.items, 0..) |exp, idx| { - if (idx > 0) std.debug.print(",", .{}); - std.debug.print("{{\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"lifecycle\":\"{s}\",\"created_at\":\"{s}\"}}", .{ exp.id, exp.name, exp.lifecycle, exp.created }); - } - std.debug.print("],\"total\":{d}}}}}\n", .{experiments.items.len}); - } else { - if (experiments.items.len == 0) { - colors.printWarning("No experiments found. Create one with: ml experiment create --name \n", .{}); - } else { - colors.printInfo("\nExperiments:\n", .{}); - colors.printInfo("{s:-<60}\n", .{""}); - for (experiments.items) |exp| { - std.debug.print("{s} | {s} | {s} | {s}\n", .{ exp.id, exp.name, exp.lifecycle, exp.created }); - } - std.debug.print("\nTotal: {d} experiments\n", .{experiments.items.len}); - } - } -} - -fn executeLog(allocator: std.mem.Allocator, args: []const []const u8, options: *const ExperimentOptions) !void { - var commit_id: ?[]const u8 = null; - var name: ?[]const u8 = null; - var value: ?f64 = null; - var step: u32 = 0; - - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (std.mem.eql(u8, arg, "--id")) { - if (i + 1 < args.len) { - commit_id = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--name")) { - if (i + 1 < args.len) { - name = args[i + 1]; - i += 1; - } - } else if (std.mem.eql(u8, arg, "--value")) { - if (i + 1 < args.len) { - value = try std.fmt.parseFloat(f64, args[i + 1]); - i += 1; - } - } else if (std.mem.eql(u8, arg, "--step")) { - if (i + 1 < args.len) { - step = try std.fmt.parseInt(u32, args[i + 1], 10); - i += 1; - } - } - } - - if (commit_id == null or name == null or value == null) { - if (options.json) { - jsonError("experiment.log", "Usage: ml experiment log --id --name --value [--step ]"); - } else { - colors.printError("Usage: ml experiment log --id --name --value [--step ]\n", .{}); - } - return; - } - - const Config = @import("../config.zig").Config; - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendLogMetric(api_key_hash, commit_id.?, name.?, value.?, step); - - if (options.json) { - const message = try client.receiveMessage(allocator); - defer allocator.free(message); - - const packet = protocol.ResponsePacket.deserialize(message, allocator) catch { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.log\",\"data\":{{\"commit_id\":\"{s}\",\"metric\":{{\"name\":\"{s}\",\"value\":{d},\"step\":{d}}},\"message\":\"{s}\"}}}}\n", - .{ commit_id.?, name.?, value.?, step, message }, - ); - return; - }; - defer packet.deinit(allocator); - - switch (packet.packet_type) { - .success => { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.log\",\"data\":{{\"commit_id\":\"{s}\",\"metric\":{{\"name\":\"{s}\",\"value\":{d},\"step\":{d}}},\"message\":\"{s}\"}}}}\n", - .{ commit_id.?, name.?, value.?, step, message }, - ); - return; - }, - else => {}, - } - } else { - try client.receiveAndHandleResponse(allocator, "Log metric"); - colors.printSuccess("Metric logged successfully!\n", .{}); - colors.printInfo("Commit ID: {s}\n", .{commit_id.?}); - colors.printInfo("Metric: {s} = {d:.4} (step {d})\n", .{ name.?, value.?, step }); - } -} - -fn executeShow(allocator: std.mem.Allocator, args: []const []const u8, options: *const ExperimentOptions) !void { - if (args.len < 1) { - if (options.json) { - jsonError("experiment.show", "Usage: ml experiment show "); - } else { - colors.printError("Usage: ml experiment show \n", .{}); - } - return; - } - - const identifier = args[0]; - const commit_id = try resolveCommitIdentifier(allocator, identifier); - defer allocator.free(commit_id); - - const Config = @import("../config.zig").Config; - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendGetExperiment(api_key_hash, commit_id); - - const message = try client.receiveMessage(allocator); - defer allocator.free(message); - - const packet = try protocol.ResponsePacket.deserialize(message, allocator); - defer packet.deinit(allocator); - - // For now, let's just print the result - switch (packet.packet_type) { - .success, .data => { - if (packet.data_payload) |payload| { - if (options.json) { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.show\",\"data\":{s}}}\n", - .{payload}, - ); - return; - } else { - // Parse JSON response - const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch |err| { - colors.printError("Failed to parse response: {}\n", .{err}); - return; - }; - defer parsed.deinit(); - - const root = parsed.value; - if (root != .object) { - colors.printError("Invalid response format\n", .{}); - return; - } - - const metadata = root.object.get("metadata"); - const metrics = root.object.get("metrics"); - - if (metadata != null and metadata.? == .object) { - colors.printInfo("\nExperiment Details:\n", .{}); - colors.printInfo("-------------------\n", .{}); - const m = metadata.?.object; - if (m.get("JobName")) |v| colors.printInfo("Job Name: {s}\n", .{v.string}); - if (m.get("CommitID")) |v| colors.printInfo("Commit ID: {s}\n", .{v.string}); - if (m.get("User")) |v| colors.printInfo("User: {s}\n", .{v.string}); - if (m.get("Timestamp")) |v| { - const ts = v.integer; - colors.printInfo("Timestamp: {d}\n", .{ts}); - } - } - - if (metrics != null and metrics.? == .array) { - colors.printInfo("\nMetrics:\n", .{}); - colors.printInfo("-------------------\n", .{}); - const items = metrics.?.array.items; - if (items.len == 0) { - colors.printInfo("No metrics logged.\n", .{}); - } else { - for (items) |item| { - if (item == .object) { - const name = item.object.get("name").?.string; - const value = item.object.get("value").?.float; - const step = item.object.get("step").?.integer; - colors.printInfo("{s}: {d:.4} (Step: {d})\n", .{ name, value, step }); - } - } - } - } - - const repro = root.object.get("reproducibility"); - if (repro != null and repro.? == .object) { - colors.printInfo("\nReproducibility:\n", .{}); - colors.printInfo("-------------------\n", .{}); - - const repro_obj = repro.?.object; - if (repro_obj.get("experiment")) |exp_val| { - if (exp_val == .object) { - const e = exp_val.object; - if (e.get("id")) |v| colors.printInfo("Experiment ID: {s}\n", .{v.string}); - if (e.get("name")) |v| colors.printInfo("Name: {s}\n", .{v.string}); - if (e.get("status")) |v| colors.printInfo("Status: {s}\n", .{v.string}); - if (e.get("user_id")) |v| colors.printInfo("User ID: {s}\n", .{v.string}); - } - } - - if (repro_obj.get("environment")) |env_val| { - if (env_val == .object) { - const env = env_val.object; - if (env.get("python_version")) |v| colors.printInfo("Python: {s}\n", .{v.string}); - if (env.get("cuda_version")) |v| colors.printInfo("CUDA: {s}\n", .{v.string}); - if (env.get("system_os")) |v| colors.printInfo("OS: {s}\n", .{v.string}); - if (env.get("system_arch")) |v| colors.printInfo("Arch: {s}\n", .{v.string}); - if (env.get("hostname")) |v| colors.printInfo("Hostname: {s}\n", .{v.string}); - if (env.get("requirements_hash")) |v| colors.printInfo("Requirements hash: {s}\n", .{v.string}); - } - } - - if (repro_obj.get("git_info")) |git_val| { - if (git_val == .object) { - const g = git_val.object; - if (g.get("commit_sha")) |v| colors.printInfo("Git SHA: {s}\n", .{v.string}); - if (g.get("branch")) |v| colors.printInfo("Git branch: {s}\n", .{v.string}); - if (g.get("remote_url")) |v| colors.printInfo("Git remote: {s}\n", .{v.string}); - if (g.get("is_dirty")) |v| colors.printInfo("Git dirty: {}\n", .{v.bool}); - } - } - - if (repro_obj.get("seeds")) |seeds_val| { - if (seeds_val == .object) { - const s = seeds_val.object; - if (s.get("numpy_seed")) |v| colors.printInfo("Numpy seed: {d}\n", .{v.integer}); - if (s.get("torch_seed")) |v| colors.printInfo("Torch seed: {d}\n", .{v.integer}); - if (s.get("tensorflow_seed")) |v| colors.printInfo("TensorFlow seed: {d}\n", .{v.integer}); - if (s.get("random_seed")) |v| colors.printInfo("Random seed: {d}\n", .{v.integer}); - } - } - } - colors.printInfo("\n", .{}); - } - } else if (packet.success_message) |msg| { - if (options.json) { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.show\",\"data\":{{\"message\":\"{s}\"}}}}\n", - .{msg}, - ); - } else { - colors.printSuccess("{s}\n", .{msg}); - } - } - }, - .error_packet => { - const code_int: u8 = if (packet.error_code) |c| @intFromEnum(c) else 0; - const default_msg = if (packet.error_code) |c| protocol.ResponsePacket.getErrorMessage(c) else "Server error"; - const err_msg = packet.error_message orelse default_msg; - const details = packet.error_details orelse ""; - if (options.json) { - std.debug.print( - "{{\"success\":false,\"command\":\"experiment.show\",\"error\":{s},\"error_code\":{d},\"error_details\":{s}}}\n", - .{ err_msg, code_int, details }, - ); - } else { - colors.printError("Error: {s}\n", .{err_msg}); - if (details.len > 0) { - colors.printError("Details: {s}\n", .{details}); - } - } - }, - else => { - if (options.json) { - jsonError("experiment.show", "Unexpected response type"); - } else { - colors.printError("Unexpected response type\n", .{}); - } - }, - } -} - -fn executeList(allocator: std.mem.Allocator, options: *const ExperimentOptions) !void { - const entries = history.loadEntries(allocator) catch |err| { - if (options.json) { - const details = try std.fmt.allocPrint(allocator, "{}", .{err}); - defer allocator.free(details); - jsonErrorWithDetails("experiment.list", "Failed to read experiment history", details); - } else { - colors.printError("Failed to read experiment history: {}\n", .{err}); - } - return err; - }; - defer history.freeEntries(allocator, entries); - - if (entries.len == 0) { - if (options.json) { - std.debug.print("{{\"success\":true,\"command\":\"experiment.list\",\"data\":{{\"experiments\":[],\"total\":0,\"message\":\"No experiments recorded yet. Use `ml queue` to submit one.\"}}}}\n", .{}); - } else { - colors.printWarning("No experiments recorded yet. Use `ml queue` to submit one.\n", .{}); - } - return; - } - - if (options.json) { - std.debug.print("{{\"success\":true,\"command\":\"experiment.list\",\"data\":{{\"experiments\":[", .{}); - var idx: usize = 0; - while (idx < entries.len) : (idx += 1) { - const entry = entries[entries.len - idx - 1]; - if (idx > 0) { - std.debug.print(",", .{}); - } - std.debug.print( - "{{\"alias\":\"{s}\",\"commit_id\":\"{s}\",\"queued_at\":{d}}}", - .{ - entry.job_name, entry.commit_id, - entry.queued_at, - }, - ); - } - std.debug.print("],\"total\":{d}", .{entries.len}); - std.debug.print("}}}}\n", .{}); - } else { - colors.printInfo("\nRecent Experiments (latest first):\n", .{}); - colors.printInfo("---------------------------------\n", .{}); - - const max_display = if (entries.len > 20) 20 else entries.len; - var idx: usize = 0; - while (idx < max_display) : (idx += 1) { - const entry = entries[entries.len - idx - 1]; - std.debug.print("{d:2}) Alias: {s}\n", .{ idx + 1, entry.job_name }); - std.debug.print(" Commit: {s}\n", .{entry.commit_id}); - std.debug.print(" Queued: {d}\n\n", .{entry.queued_at}); - } - - if (entries.len > max_display) { - colors.printInfo("...and {d} more\n", .{entries.len - max_display}); - } - } -} - -fn executeDelete(allocator: std.mem.Allocator, identifier: []const u8, options: *const ExperimentOptions) !void { - const resolved = try resolveJobIdentifier(allocator, identifier); - defer allocator.free(resolved); - - if (options.json) { - const Config = @import("../config.zig").Config; - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendCancelJob(resolved, api_key_hash); - const message = try client.receiveMessage(allocator); - defer allocator.free(message); - - // Prefer parsing structured binary response packets if present. - if (message.len > 0) { - const packet = protocol.ResponsePacket.deserialize(message, allocator) catch null; - if (packet) |p| { - defer p.deinit(allocator); - - switch (p.packet_type) { - .success => { - const msg = p.success_message orelse ""; - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.delete\",\"data\":{{\"experiment\":\"{s}\",\"message\":\"{s}\"}}}}\n", - .{ resolved, msg }, - ); - return; - }, - .error_packet => { - const code_int: u8 = if (p.error_code) |c| @intFromEnum(c) else 0; - const default_msg = if (p.error_code) |c| protocol.ResponsePacket.getErrorMessage(c) else "Server error"; - const err_msg = p.error_message orelse default_msg; - const details = p.error_details orelse ""; - std.debug.print("{{\"success\":false,\"command\":\"experiment.delete\",\"error\":\"{s}\",\"error_code\":{d},\"error_details\":\"{s}\",\"data\":{{\"experiment\":\"{s}\"}}}}\n", .{ err_msg, code_int, details, resolved }); - return error.CommandFailed; - }, - else => {}, - } - } - } - - // Next: if server returned JSON, wrap it and attempt to infer success. - if (message.len > 0 and message[0] == '{') { - const parsed = std.json.parseFromSlice(std.json.Value, allocator, message, .{}) catch { - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.delete\",\"data\":{{\"experiment\":\"{s}\",\"response\":{s}}}}}\n", - .{ resolved, message }, - ); - return; - }; - defer parsed.deinit(); - - if (parsed.value == .object) { - if (parsed.value.object.get("success")) |sval| { - if (sval == .bool and !sval.bool) { - const err_val = parsed.value.object.get("error"); - const err_msg = if (err_val != null and err_val.? == .string) err_val.?.string else "Failed to cancel experiment"; - std.debug.print( - "{{\"success\":false,\"command\":\"experiment.delete\",\"error\":\"{s}\",\"data\":{{\"experiment\":\"{s}\",\"response\":{s}}}}}\n", - .{ err_msg, resolved, message }, - ); - return error.CommandFailed; - } - } - } - - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.delete\",\"data\":{{\"experiment\":\"{s}\",\"response\":{s}}}}}\n", - .{ resolved, message }, - ); - return; - } - - // Fallback: plain string message. - std.debug.print( - "{{\"success\":true,\"command\":\"experiment.delete\",\"data\":{{\"experiment\":\"{s}\",\"message\":\"{s}\"}}}}\n", - .{ resolved, message }, - ); - return; - } - - // Build cancel args with JSON flag if needed - var cancel_args = std.ArrayList([]const u8).initCapacity(allocator, 5) catch |err| { - return err; - }; - defer cancel_args.deinit(allocator); - - try cancel_args.append(allocator, resolved); - - cancel_cmd.run(allocator, cancel_args.items) catch |err| { - colors.printError("Failed to cancel experiment '{s}': {}\n", .{ resolved, err }); - return err; - }; -} - -fn resolveCommitIdentifier(allocator: std.mem.Allocator, identifier: []const u8) ![]const u8 { - const entries = history.loadEntries(allocator) catch { - if (identifier.len != 40) return error.InvalidCommitId; - const commit_bytes = try crypto.decodeHex(allocator, identifier); - if (commit_bytes.len != 20) { - allocator.free(commit_bytes); - return error.InvalidCommitId; - } - return commit_bytes; - }; - defer history.freeEntries(allocator, entries); - - var commit_hex: []const u8 = identifier; - for (entries) |entry| { - if (std.mem.eql(u8, identifier, entry.job_name)) { - commit_hex = entry.commit_id; - break; - } - } - - if (commit_hex.len != 40) return error.InvalidCommitId; - const commit_bytes = try crypto.decodeHex(allocator, commit_hex); - if (commit_bytes.len != 20) { - allocator.free(commit_bytes); - return error.InvalidCommitId; - } - return commit_bytes; -} - -fn resolveJobIdentifier(allocator: std.mem.Allocator, identifier: []const u8) ![]const u8 { - const entries = history.loadEntries(allocator) catch { - return allocator.dupe(u8, identifier); - }; - defer history.freeEntries(allocator, entries); - - for (entries) |entry| { - if (std.mem.eql(u8, identifier, entry.job_name) or - std.mem.eql(u8, identifier, entry.commit_id) or - (identifier.len <= entry.commit_id.len and - std.mem.eql(u8, entry.commit_id[0..identifier.len], identifier))) - { - return allocator.dupe(u8, entry.job_name); - } - } - - return allocator.dupe(u8, identifier); -} diff --git a/cli/src/commands/logs.zig b/cli/src/commands/logs.zig deleted file mode 100644 index 8f14932..0000000 --- a/cli/src/commands/logs.zig +++ /dev/null @@ -1,134 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); - -/// Logs command - fetch and display job logs via WebSocket API -pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void { - if (argv.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - if (std.mem.eql(u8, argv[0], "--help") or std.mem.eql(u8, argv[0], "-h")) { - try printUsage(); - return; - } - - const target = argv[0]; - - // Parse optional flags - var follow = false; - var tail: ?usize = null; - - var i: usize = 1; - while (i < argv.len) : (i += 1) { - const a = argv[i]; - if (std.mem.eql(u8, a, "-f") or std.mem.eql(u8, a, "--follow")) { - follow = true; - } else if (std.mem.eql(u8, a, "-n") and i + 1 < argv.len) { - tail = try std.fmt.parseInt(usize, argv[i + 1], 10); - i += 1; - } else if (std.mem.eql(u8, a, "--tail") and i + 1 < argv.len) { - tail = try std.fmt.parseInt(usize, argv[i + 1], 10); - i += 1; - } else { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - colors.printInfo("Fetching logs for: {s}\n", .{target}); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - // Send appropriate request based on follow flag - if (follow) { - try client.sendStreamLogs(target, api_key_hash); - } else { - try client.sendGetLogs(target, api_key_hash); - } - - // Receive and display response - const message = try client.receiveMessage(allocator); - defer allocator.free(message); - - const packet = protocol.ResponsePacket.deserialize(message, allocator) catch { - // Fallback: treat as plain text response - std.debug.print("{s}\n", .{message}); - return; - }; - defer packet.deinit(allocator); - - switch (packet.packet_type) { - .data => { - if (packet.data_payload) |payload| { - // Parse JSON response - const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch { - std.debug.print("{s}\n", .{payload}); - return; - }; - defer parsed.deinit(); - - const root = parsed.value.object; - - // Display logs - if (root.get("logs")) |logs| { - if (logs == .string) { - std.debug.print("{s}\n", .{logs.string}); - } - } else if (root.get("message")) |msg| { - if (msg == .string) { - colors.printInfo("{s}\n", .{msg.string}); - } - } - - // Show truncation warning if applicable - if (root.get("truncated")) |truncated| { - if (truncated == .bool and truncated.bool) { - if (root.get("total_lines")) |total| { - if (total == .integer) { - colors.printWarning("\n[Output truncated. Total lines: {d}]\n", .{total.integer}); - } - } - } - } - } - }, - .error_packet => { - const err_msg = packet.error_message orelse "Unknown error"; - colors.printError("Error: {s}\n", .{err_msg}); - return error.ServerError; - }, - else => { - if (packet.success_message) |msg| { - colors.printSuccess("{s}\n", .{msg}); - } else { - colors.printInfo("Logs retrieved successfully\n", .{}); - } - }, - } -} - -fn printUsage() !void { - colors.printInfo("Usage:\n", .{}); - colors.printInfo(" ml logs [-f|--follow] [-n |--tail ]\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml logs abc123 # Show full logs\n", .{}); - colors.printInfo(" ml logs abc123 -f # Follow logs in real-time\n", .{}); - colors.printInfo(" ml logs abc123 -n 100 # Show last 100 lines\n", .{}); -} diff --git a/cli/src/commands/monitor.zig b/cli/src/commands/monitor.zig deleted file mode 100644 index 0c2908f..0000000 --- a/cli/src/commands/monitor.zig +++ /dev/null @@ -1,69 +0,0 @@ -const std = @import("std"); -const Config = @import("../config.zig").Config; - -pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { - for (args) |arg| { - if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - printUsage(); - return; - } - if (std.mem.eql(u8, arg, "--json")) { - std.debug.print("monitor does not support --json\n", .{}); - printUsage(); - return error.InvalidArgs; - } - } - - const config = try Config.load(allocator); - defer { - var mut_config = config; - mut_config.deinit(allocator); - } - - std.debug.print("Launching TUI via SSH...\n", .{}); - - // Build remote command that exports config via env vars and runs the TUI - var remote_cmd_buffer = std.ArrayList(u8){}; - defer remote_cmd_buffer.deinit(allocator); - { - const writer = remote_cmd_buffer.writer(allocator); - try writer.print("cd {s} && ", .{config.worker_base}); - try writer.print( - "FETCH_ML_CLI_HOST=\"{s}\" FETCH_ML_CLI_USER=\"{s}\" FETCH_ML_CLI_BASE=\"{s}\" ", - .{ config.worker_host, config.worker_user, config.worker_base }, - ); - try writer.print( - "FETCH_ML_CLI_PORT=\"{d}\" FETCH_ML_CLI_API_KEY=\"{s}\" ", - .{ config.worker_port, config.api_key }, - ); - try writer.writeAll("./bin/tui"); - for (args) |arg| { - try writer.print(" {s}", .{arg}); - } - } - const remote_cmd = try remote_cmd_buffer.toOwnedSlice(allocator); - defer allocator.free(remote_cmd); - - const ssh_cmd = try std.fmt.allocPrint( - allocator, - "ssh -t -p {d} {s}@{s} '{s}'", - .{ config.worker_port, config.worker_user, config.worker_host, remote_cmd }, - ); - defer allocator.free(ssh_cmd); - - var child = std.process.Child.init(&[_][]const u8{ "sh", "-c", ssh_cmd }, allocator); - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; - - const term = try child.spawnAndWait(); - - if (term.tag == .Exited and term.Exited != 0) { - std.debug.print("TUI exited with code {d}\n", .{term.Exited}); - } -} - -fn printUsage() void { - std.debug.print("Usage: ml monitor [-- ]\n\n", .{}); - std.debug.print("Launches the remote TUI over SSH using ~/.ml/config.toml\n", .{}); -} diff --git a/cli/src/commands/narrative.zig b/cli/src/commands/narrative.zig deleted file mode 100644 index 908b7c6..0000000 --- a/cli/src/commands/narrative.zig +++ /dev/null @@ -1,251 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const io = @import("../utils/io.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const manifest = @import("../utils/manifest.zig"); - -pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void { - if (argv.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - - const sub = argv[0]; - if (std.mem.eql(u8, sub, "--help") or std.mem.eql(u8, sub, "-h")) { - try printUsage(); - return; - } - - if (!std.mem.eql(u8, sub, "set")) { - colors.printError("Unknown subcommand: {s}\n", .{sub}); - try printUsage(); - return error.InvalidArgs; - } - - if (argv.len < 2) { - try printUsage(); - return error.InvalidArgs; - } - - const target = argv[1]; - - var hypothesis: ?[]const u8 = null; - var context: ?[]const u8 = null; - var intent: ?[]const u8 = null; - var expected_outcome: ?[]const u8 = null; - var parent_run: ?[]const u8 = null; - var experiment_group: ?[]const u8 = null; - var tags_csv: ?[]const u8 = null; - var base_override: ?[]const u8 = null; - var json_mode: bool = false; - - var i: usize = 2; - while (i < argv.len) : (i += 1) { - const a = argv[i]; - if (std.mem.eql(u8, a, "--hypothesis")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - hypothesis = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--context")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - context = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--intent")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - intent = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--expected-outcome")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - expected_outcome = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--parent-run")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - parent_run = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--experiment-group")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - experiment_group = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--tags")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - tags_csv = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--base")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - base_override = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--json")) { - json_mode = true; - } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { - try printUsage(); - return; - } else { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - if (hypothesis == null and context == null and intent == null and expected_outcome == null and parent_run == null and experiment_group == null and tags_csv == null) { - colors.printError("No narrative fields provided.\n", .{}); - return error.InvalidArgs; - } - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const resolved_base = base_override orelse cfg.worker_base; - const manifest_path = manifest.resolvePathWithBase(allocator, target, resolved_base) catch |err| { - if (err == error.FileNotFound) { - colors.printError( - "Could not locate run_manifest.json for '{s}'. Provide a path, or use --base to scan finished/failed/running/pending.\n", - .{target}, - ); - } - return err; - }; - defer allocator.free(manifest_path); - - const job_name = try manifest.readJobNameFromManifest(allocator, manifest_path); - defer allocator.free(job_name); - - const patch_json = try buildPatchJSON( - allocator, - hypothesis, - context, - intent, - expected_outcome, - parent_run, - experiment_group, - tags_csv, - ); - defer allocator.free(patch_json); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendSetRunNarrative(job_name, patch_json, api_key_hash); - - if (json_mode) { - const msg = try client.receiveMessage(allocator); - defer allocator.free(msg); - - const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch { - var out = io.stdoutWriter(); - try out.print("{s}\n", .{msg}); - return error.InvalidPacket; - }; - defer packet.deinit(allocator); - - const Result = struct { - ok: bool, - job_name: []const u8, - message: []const u8, - error_code: ?u8 = null, - error_message: ?[]const u8 = null, - details: ?[]const u8 = null, - }; - - var out = io.stdoutWriter(); - if (packet.packet_type == .error_packet) { - const res = Result{ - .ok = false, - .job_name = job_name, - .message = "", - .error_code = @intFromEnum(packet.error_code.?), - .error_message = packet.error_message orelse "", - .details = packet.error_details orelse "", - }; - try out.print("{f}\n", .{std.json.fmt(res, .{})}); - return error.CommandFailed; - } - - const res = Result{ - .ok = true, - .job_name = job_name, - .message = packet.success_message orelse "", - }; - try out.print("{f}\n", .{std.json.fmt(res, .{})}); - return; - } - - try client.receiveAndHandleResponse(allocator, "Narrative"); - - colors.printSuccess("Narrative updated\n", .{}); - colors.printInfo("Job: {s}\n", .{job_name}); -} - -fn buildPatchJSON( - allocator: std.mem.Allocator, - hypothesis: ?[]const u8, - context: ?[]const u8, - intent: ?[]const u8, - expected_outcome: ?[]const u8, - parent_run: ?[]const u8, - experiment_group: ?[]const u8, - tags_csv: ?[]const u8, -) ![]u8 { - var out = std.ArrayList(u8).initCapacity(allocator, 256) catch return error.OutOfMemory; - defer out.deinit(allocator); - - var tags_list = std.ArrayList([]const u8).initCapacity(allocator, 8) catch return error.OutOfMemory; - defer tags_list.deinit(allocator); - - if (tags_csv) |csv| { - var it = std.mem.splitScalar(u8, csv, ','); - while (it.next()) |part| { - const trimmed = std.mem.trim(u8, part, " \t\r\n"); - if (trimmed.len == 0) continue; - try tags_list.append(allocator, trimmed); - } - } - - const Patch = struct { - hypothesis: ?[]const u8 = null, - context: ?[]const u8 = null, - intent: ?[]const u8 = null, - expected_outcome: ?[]const u8 = null, - parent_run: ?[]const u8 = null, - experiment_group: ?[]const u8 = null, - tags: ?[]const []const u8 = null, - }; - - const patch = Patch{ - .hypothesis = hypothesis, - .context = context, - .intent = intent, - .expected_outcome = expected_outcome, - .parent_run = parent_run, - .experiment_group = experiment_group, - .tags = if (tags_list.items.len > 0) tags_list.items else null, - }; - - const writer = out.writer(allocator); - try writer.print("{f}", .{std.json.fmt(patch, .{})}); - return out.toOwnedSlice(allocator); -} - -fn printUsage() !void { - colors.printInfo("Usage: ml narrative set [fields]\n", .{}); - colors.printInfo("\nFields:\n", .{}); - colors.printInfo(" --hypothesis \"...\"\n", .{}); - colors.printInfo(" --context \"...\"\n", .{}); - colors.printInfo(" --intent \"...\"\n", .{}); - colors.printInfo(" --expected-outcome \"...\"\n", .{}); - colors.printInfo(" --parent-run \n", .{}); - colors.printInfo(" --experiment-group \n", .{}); - colors.printInfo(" --tags a,b,c\n", .{}); - colors.printInfo(" --base \n", .{}); - colors.printInfo(" --json\n", .{}); -} diff --git a/cli/src/commands/outcome.zig b/cli/src/commands/outcome.zig deleted file mode 100644 index f79388f..0000000 --- a/cli/src/commands/outcome.zig +++ /dev/null @@ -1,314 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const io = @import("../utils/io.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const manifest = @import("../utils/manifest.zig"); - -pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void { - if (argv.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - - const sub = argv[0]; - if (std.mem.eql(u8, sub, "--help") or std.mem.eql(u8, sub, "-h")) { - try printUsage(); - return; - } - - if (!std.mem.eql(u8, sub, "set")) { - colors.printError("Unknown subcommand: {s}\n", .{sub}); - try printUsage(); - return error.InvalidArgs; - } - - if (argv.len < 2) { - try printUsage(); - return error.InvalidArgs; - } - - const target = argv[1]; - - var outcome_status: ?[]const u8 = null; - var outcome_summary: ?[]const u8 = null; - var learnings = std.ArrayList([]const u8).empty; - defer learnings.deinit(allocator); - var next_steps = std.ArrayList([]const u8).empty; - defer next_steps.deinit(allocator); - var validation_status: ?[]const u8 = null; - var surprises = std.ArrayList([]const u8).empty; - defer surprises.deinit(allocator); - var base_override: ?[]const u8 = null; - var json_mode: bool = false; - - var i: usize = 2; - while (i < argv.len) : (i += 1) { - const a = argv[i]; - if (std.mem.eql(u8, a, "--outcome") or std.mem.eql(u8, a, "--status")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - outcome_status = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--summary")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - outcome_summary = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--learning")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - try learnings.append(allocator, argv[i + 1]); - i += 1; - } else if (std.mem.eql(u8, a, "--next-step")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - try next_steps.append(allocator, argv[i + 1]); - i += 1; - } else if (std.mem.eql(u8, a, "--validation-status")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - validation_status = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--surprise")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - try surprises.append(allocator, argv[i + 1]); - i += 1; - } else if (std.mem.eql(u8, a, "--base")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - base_override = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--json")) { - json_mode = true; - } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { - try printUsage(); - return; - } else { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - if (outcome_status == null and outcome_summary == null and learnings.items.len == 0 and - next_steps.items.len == 0 and validation_status == null and surprises.items.len == 0) - { - colors.printError("No outcome fields provided.\n", .{}); - return error.InvalidArgs; - } - - // Validate outcome status if provided - if (outcome_status) |os| { - const valid = std.mem.eql(u8, os, "validates") or - std.mem.eql(u8, os, "refutes") or - std.mem.eql(u8, os, "inconclusive") or - std.mem.eql(u8, os, "partial") or - std.mem.eql(u8, os, "inconclusive-partial"); - if (!valid) { - colors.printError("Invalid outcome status: {s}. Must be one of: validates, refutes, inconclusive, partial\n", .{os}); - return error.InvalidArgs; - } - } - - // Validate validation status if provided - if (validation_status) |vs| { - const valid = std.mem.eql(u8, vs, "validates") or - std.mem.eql(u8, vs, "refutes") or - std.mem.eql(u8, vs, "inconclusive") or - std.mem.eql(u8, vs, ""); - if (!valid) { - colors.printError("Invalid validation status: {s}. Must be one of: validates, refutes, inconclusive\n", .{vs}); - return error.InvalidArgs; - } - } - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const resolved_base = base_override orelse cfg.worker_base; - const manifest_path = manifest.resolvePathWithBase(allocator, target, resolved_base) catch |err| { - if (err == error.FileNotFound) { - colors.printError( - "Could not locate run_manifest.json for '{s}'. Provide a path, or use --base to scan finished/failed/running/pending.\n", - .{target}, - ); - } - return err; - }; - defer allocator.free(manifest_path); - - const job_name = try manifest.readJobNameFromManifest(allocator, manifest_path); - defer allocator.free(job_name); - - const patch_json = try buildOutcomePatchJSON( - allocator, - outcome_status, - outcome_summary, - learnings.items, - next_steps.items, - validation_status, - surprises.items, - ); - defer allocator.free(patch_json); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendSetRunNarrative(job_name, patch_json, api_key_hash); - - if (json_mode) { - const msg = try client.receiveMessage(allocator); - defer allocator.free(msg); - - const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch { - var out = io.stdoutWriter(); - try out.print("{s}\n", .{msg}); - return error.InvalidPacket; - }; - defer packet.deinit(allocator); - - if (packet.packet_type == .success) { - var out = io.stdoutWriter(); - try out.print("{{\"success\":true,\"job_name\":\"{s}\"}}\n", .{job_name}); - } else if (packet.packet_type == .error_packet) { - var out = io.stdoutWriter(); - try out.print("{{\"success\":false,\"error\":\"{s}\"}}\n", .{packet.error_message orelse "unknown"}); - } else { - var out = io.stdoutWriter(); - try out.print("{{\"success\":true,\"job_name\":\"{s}\",\"response\":\"{s}\"}}\n", .{ job_name, packet.success_message orelse "ok" }); - } - } else { - try client.receiveAndHandleResponse(allocator, "Outcome set"); - } -} - -fn printUsage() !void { - colors.printInfo("Usage: ml outcome set [options]\n", .{}); - colors.printInfo("\nPost-Run Outcome Capture:\n", .{}); - colors.printInfo(" --outcome Outcome: validates|refutes|inconclusive|partial\n", .{}); - colors.printInfo(" --summary Summary of results\n", .{}); - colors.printInfo(" --learning A learning from this run (can repeat)\n", .{}); - colors.printInfo(" --next-step Suggested next step (can repeat)\n", .{}); - colors.printInfo(" --validation-status Did results validate hypothesis? validates|refutes|inconclusive\n", .{}); - colors.printInfo(" --surprise Unexpected finding (can repeat)\n", .{}); - colors.printInfo("\nOptions:\n", .{}); - colors.printInfo(" --base Base path to search for run_manifest.json\n", .{}); - colors.printInfo(" --json Output JSON response\n", .{}); - colors.printInfo(" --help, -h Show this help\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml outcome set run_abc --outcome validates --summary \"Accuracy +0.02\"\n", .{}); - colors.printInfo(" ml outcome set run_abc --learning \"LR scaling worked\" --learning \"GPU util 95%\"\n", .{}); - colors.printInfo(" ml outcome set run_abc --outcome validates --next-step \"Try batch=96\"\n", .{}); -} - -fn buildOutcomePatchJSON( - allocator: std.mem.Allocator, - outcome_status: ?[]const u8, - outcome_summary: ?[]const u8, - learnings: [][]const u8, - next_steps: [][]const u8, - validation_status: ?[]const u8, - surprises: [][]const u8, -) ![]u8 { - var buf = std.ArrayList(u8).empty; - defer buf.deinit(allocator); - - const writer = buf.writer(allocator); - try writer.writeAll("{\"narrative\":"); - try writer.writeAll("{"); - - var first = true; - - if (outcome_status) |os| { - if (!first) try writer.writeAll(","); - first = false; - try writer.print("\"outcome\":\"{s}\"", .{os}); - } - - if (outcome_summary) |os| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"outcome_summary\":"); - try writeJSONString(writer, os); - } - - if (learnings.len > 0) { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"learnings\":["); - for (learnings, 0..) |learning, idx| { - if (idx > 0) try writer.writeAll(","); - try writeJSONString(writer, learning); - } - try writer.writeAll("]"); - } - - if (next_steps.len > 0) { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"next_steps\":["); - for (next_steps, 0..) |step, idx| { - if (idx > 0) try writer.writeAll(","); - try writeJSONString(writer, step); - } - try writer.writeAll("]"); - } - - if (validation_status) |vs| { - if (!first) try writer.writeAll(","); - first = false; - try writer.print("\"validation_status\":\"{s}\"", .{vs}); - } - - if (surprises.len > 0) { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"surprises\":["); - for (surprises, 0..) |surprise, idx| { - if (idx > 0) try writer.writeAll(","); - try writeJSONString(writer, surprise); - } - try writer.writeAll("]"); - } - - try writer.writeAll("}}"); - - return buf.toOwnedSlice(allocator); -} - -fn writeJSONString(writer: anytype, s: []const u8) !void { - try writer.writeAll("\""); - for (s) |c| { - switch (c) { - '"' => try writer.writeAll("\\\""), - '\\' => try writer.writeAll("\\\\"), - '\n' => try writer.writeAll("\\n"), - '\r' => try writer.writeAll("\\r"), - '\t' => try writer.writeAll("\\t"), - else => { - if (c < 0x20) { - var buf: [6]u8 = undefined; - buf[0] = '\\'; - buf[1] = 'u'; - buf[2] = '0'; - buf[3] = '0'; - buf[4] = hexDigit(@intCast((c >> 4) & 0x0F)); - buf[5] = hexDigit(@intCast(c & 0x0F)); - try writer.writeAll(&buf); - } else { - try writer.writeAll(&[_]u8{c}); - } - }, - } - } - try writer.writeAll("\""); -} - -fn hexDigit(v: u8) u8 { - return if (v < 10) ('0' + v) else ('a' + (v - 10)); -} diff --git a/cli/src/commands/privacy.zig b/cli/src/commands/privacy.zig deleted file mode 100644 index e25e8bb..0000000 --- a/cli/src/commands/privacy.zig +++ /dev/null @@ -1,241 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const io = @import("../utils/io.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const manifest = @import("../utils/manifest.zig"); - -pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void { - if (argv.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - - const sub = argv[0]; - if (std.mem.eql(u8, sub, "--help") or std.mem.eql(u8, sub, "-h")) { - try printUsage(); - return; - } - - if (!std.mem.eql(u8, sub, "set")) { - colors.printError("Unknown subcommand: {s}\n", .{sub}); - try printUsage(); - return error.InvalidArgs; - } - - if (argv.len < 2) { - try printUsage(); - return error.InvalidArgs; - } - - const target = argv[1]; - - var privacy_level: ?[]const u8 = null; - var team: ?[]const u8 = null; - var owner: ?[]const u8 = null; - var base_override: ?[]const u8 = null; - var json_mode: bool = false; - - var i: usize = 2; - while (i < argv.len) : (i += 1) { - const a = argv[i]; - if (std.mem.eql(u8, a, "--level") or std.mem.eql(u8, a, "--privacy-level")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - privacy_level = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--team")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - team = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--owner")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - owner = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--base")) { - if (i + 1 >= argv.len) return error.InvalidArgs; - base_override = argv[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--json")) { - json_mode = true; - } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { - try printUsage(); - return; - } else { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - if (privacy_level == null and team == null and owner == null) { - colors.printError("No privacy fields provided.\n", .{}); - return error.InvalidArgs; - } - - // Validate privacy level if provided - if (privacy_level) |pl| { - const valid = std.mem.eql(u8, pl, "private") or - std.mem.eql(u8, pl, "team") or - std.mem.eql(u8, pl, "public") or - std.mem.eql(u8, pl, "anonymized"); - if (!valid) { - colors.printError("Invalid privacy level: {s}. Must be one of: private, team, public, anonymized\n", .{pl}); - return error.InvalidArgs; - } - } - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - const resolved_base = base_override orelse cfg.worker_base; - const manifest_path = manifest.resolvePathWithBase(allocator, target, resolved_base) catch |err| { - if (err == error.FileNotFound) { - colors.printError( - "Could not locate run_manifest.json for '{s}'. Provide a path, or use --base to scan finished/failed/running/pending.\n", - .{target}, - ); - } - return err; - }; - defer allocator.free(manifest_path); - - const job_name = try manifest.readJobNameFromManifest(allocator, manifest_path); - defer allocator.free(job_name); - - const patch_json = try buildPrivacyPatchJSON( - allocator, - privacy_level, - team, - owner, - ); - defer allocator.free(patch_json); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - try client.sendSetRunPrivacy(job_name, patch_json, api_key_hash); - - if (json_mode) { - const msg = try client.receiveMessage(allocator); - defer allocator.free(msg); - - const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch { - var out = io.stdoutWriter(); - try out.print("{s}\n", .{msg}); - return error.InvalidPacket; - }; - defer packet.deinit(allocator); - - if (packet.packet_type == .success) { - var out = io.stdoutWriter(); - try out.print("{{\"success\":true,\"job_name\":\"{s}\"}}\n", .{job_name}); - } else if (packet.packet_type == .error_packet) { - var out = io.stdoutWriter(); - try out.print("{{\"success\":false,\"error\":\"{s}\"}}\n", .{packet.error_message orelse "unknown"}); - } - } else { - try client.receiveAndHandleResponse(allocator, "Privacy set"); - } -} - -fn printUsage() !void { - colors.printInfo("Usage: ml privacy set [options]\n", .{}); - colors.printInfo("\nPrivacy Levels:\n", .{}); - colors.printInfo(" private Owner only (default)\n", .{}); - colors.printInfo(" team Same-team members can view\n", .{}); - colors.printInfo(" public All authenticated users\n", .{}); - colors.printInfo(" anonymized Strip PII before sharing\n", .{}); - colors.printInfo("\nOptions:\n", .{}); - colors.printInfo(" --level Set privacy level\n", .{}); - colors.printInfo(" --team Set team name\n", .{}); - colors.printInfo(" --owner Set owner email\n", .{}); - colors.printInfo(" --base Base path to search for run_manifest.json\n", .{}); - colors.printInfo(" --json Output JSON response\n", .{}); - colors.printInfo(" --help, -h Show this help\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml privacy set run_abc --level team --team vision-research\n", .{}); - colors.printInfo(" ml privacy set run_abc --level private\n", .{}); - colors.printInfo(" ml privacy set run_abc --owner user@lab.edu --team ml-group\n", .{}); -} - -fn buildPrivacyPatchJSON( - allocator: std.mem.Allocator, - privacy_level: ?[]const u8, - team: ?[]const u8, - owner: ?[]const u8, -) ![]u8 { - var buf = std.ArrayList(u8).empty; - defer buf.deinit(allocator); - - const writer = buf.writer(allocator); - try writer.writeAll("{\"privacy\":"); - try writer.writeAll("{"); - - var first = true; - - if (privacy_level) |pl| { - if (!first) try writer.writeAll(","); - first = false; - try writer.print("\"level\":\"{s}\"", .{pl}); - } - - if (team) |t| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"team\":"); - try writeJSONString(writer, t); - } - - if (owner) |o| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"owner\":"); - try writeJSONString(writer, o); - } - - try writer.writeAll("}}"); - - return buf.toOwnedSlice(allocator); -} - -fn writeJSONString(writer: anytype, s: []const u8) !void { - try writer.writeAll("\""); - for (s) |c| { - switch (c) { - '"' => try writer.writeAll("\\\""), - '\\' => try writer.writeAll("\\\\"), - '\n' => try writer.writeAll("\\n"), - '\r' => try writer.writeAll("\\r"), - '\t' => try writer.writeAll("\\t"), - else => { - if (c < 0x20) { - var buf: [6]u8 = undefined; - buf[0] = '\\'; - buf[1] = 'u'; - buf[2] = '0'; - buf[3] = '0'; - buf[4] = hexDigit(@intCast((c >> 4) & 0x0F)); - buf[5] = hexDigit(@intCast(c & 0x0F)); - try writer.writeAll(&buf); - } else { - try writer.writeAll(&[_]u8{c}); - } - }, - } - } - try writer.writeAll("\""); -} - -fn hexDigit(v: u8) u8 { - return if (v < 10) ('0' + v) else ('a' + (v - 10)); -} diff --git a/cli/src/commands/requeue.zig b/cli/src/commands/requeue.zig deleted file mode 100644 index aaf65fd..0000000 --- a/cli/src/commands/requeue.zig +++ /dev/null @@ -1,523 +0,0 @@ -const std = @import("std"); -const colors = @import("../utils/colors.zig"); -const Config = @import("../config.zig").Config; -const crypto = @import("../utils/crypto.zig"); -const ws = @import("../net/ws/client.zig"); -const protocol = @import("../net/protocol.zig"); -const manifest = @import("../utils/manifest.zig"); -const json = @import("../utils/json.zig"); - -pub fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void { - if (argv.len == 0) { - try printUsage(); - return error.InvalidArgs; - } - if (std.mem.eql(u8, argv[0], "--help") or std.mem.eql(u8, argv[0], "-h")) { - try printUsage(); - return; - } - - const target = argv[0]; - - // Split args at "--". - var sep_index: ?usize = null; - for (argv, 0..) |a, i| { - if (std.mem.eql(u8, a, "--")) { - sep_index = i; - break; - } - } - const pre = argv[1..(sep_index orelse argv.len)]; - const post = if (sep_index) |i| argv[(i + 1)..] else argv[0..0]; - - const cfg = try Config.load(allocator); - defer { - var mut_cfg = cfg; - mut_cfg.deinit(allocator); - } - - // Defaults - var job_name_override: ?[]const u8 = null; - var priority: u8 = cfg.default_priority; - var cpu: u8 = cfg.default_cpu; - var memory: u8 = cfg.default_memory; - var gpu: u8 = cfg.default_gpu; - var gpu_memory: ?[]const u8 = cfg.default_gpu_memory; - var args_override: ?[]const u8 = null; - var note_override: ?[]const u8 = null; - var force: bool = false; - - // New: Change tracking options - var inherit_narrative: bool = false; - var inherit_config: bool = false; - var parent_link: bool = false; - var overrides = std.ArrayList([2][]const u8).empty; // [key, value] pairs - defer overrides.deinit(allocator); - - var i: usize = 0; - while (i < pre.len) : (i += 1) { - const a = pre[i]; - if (std.mem.eql(u8, a, "--name") and i + 1 < pre.len) { - job_name_override = pre[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--priority") and i + 1 < pre.len) { - priority = try std.fmt.parseInt(u8, pre[i + 1], 10); - i += 1; - } else if (std.mem.eql(u8, a, "--cpu") and i + 1 < pre.len) { - cpu = try std.fmt.parseInt(u8, pre[i + 1], 10); - i += 1; - } else if (std.mem.eql(u8, a, "--memory") and i + 1 < pre.len) { - memory = try std.fmt.parseInt(u8, pre[i + 1], 10); - i += 1; - } else if (std.mem.eql(u8, a, "--gpu") and i + 1 < pre.len) { - gpu = try std.fmt.parseInt(u8, pre[i + 1], 10); - i += 1; - } else if (std.mem.eql(u8, a, "--gpu-memory") and i + 1 < pre.len) { - gpu_memory = pre[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--args") and i + 1 < pre.len) { - args_override = pre[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--note") and i + 1 < pre.len) { - note_override = pre[i + 1]; - i += 1; - } else if (std.mem.eql(u8, a, "--force")) { - force = true; - } else if (std.mem.eql(u8, a, "--inherit-narrative")) { - inherit_narrative = true; - } else if (std.mem.eql(u8, a, "--inherit-config")) { - inherit_config = true; - } else if (std.mem.eql(u8, a, "--parent")) { - parent_link = true; - } else if (std.mem.startsWith(u8, a, "--") and std.mem.indexOf(u8, a, "=") != null) { - // Key=value override: --lr=0.002 or --batch-size=128 - const eq_idx = std.mem.indexOf(u8, a, "=").?; - const key = a[2..eq_idx]; - const value = a[eq_idx + 1 ..]; - try overrides.append(allocator, [2][]const u8{ key, value }); - } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { - try printUsage(); - return; - } else { - colors.printError("Unknown option: {s}\n", .{a}); - return error.InvalidArgs; - } - } - - var args_joined: []const u8 = ""; - if (post.len > 0) { - var buf: std.ArrayList(u8) = .{}; - defer buf.deinit(allocator); - for (post, 0..) |a, idx| { - if (idx > 0) try buf.append(allocator, ' '); - try buf.appendSlice(allocator, a); - } - args_joined = try buf.toOwnedSlice(allocator); - } - defer if (post.len > 0) allocator.free(args_joined); - - const args_final: []const u8 = if (args_override) |a| a else args_joined; - const note_final: []const u8 = if (note_override) |n| n else ""; - - // Read original manifest for inheritance - var original_narrative: ?std.json.ObjectMap = null; - var original_config: ?std.json.ObjectMap = null; - var parent_run_id: ?[]const u8 = null; - - // Target can be: - // - commit_id (40-hex) or commit_id prefix (>=7 hex) resolvable under worker_base - // - run_id/task_id/path (resolved to run_manifest.json to read commit_id) - var commit_hex: []const u8 = ""; - var commit_hex_owned: ?[]u8 = null; - defer if (commit_hex_owned) |s| allocator.free(s); - - var commit_bytes: []u8 = &[_]u8{}; - var commit_bytes_allocated = false; - defer if (commit_bytes_allocated) allocator.free(commit_bytes); - - // If we need to inherit narrative or config, or link parent, read the manifest first - if (inherit_narrative or inherit_config or parent_link) { - const manifest_path = try manifest.resolvePathWithBase(allocator, target, cfg.worker_base); - defer allocator.free(manifest_path); - - const data = try manifest.readFileAlloc(allocator, manifest_path); - defer allocator.free(data); - - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, data, .{}); - defer parsed.deinit(); - - if (parsed.value == .object) { - const root = parsed.value.object; - - if (inherit_narrative) { - if (root.get("narrative")) |narr| { - if (narr == .object) { - original_narrative = try narr.object.clone(); - } - } - } - - if (inherit_config) { - if (root.get("metadata")) |meta| { - if (meta == .object) { - original_config = try meta.object.clone(); - } - } - } - - if (parent_link) { - parent_run_id = json.getString(root, "run_id") orelse json.getString(root, "id"); - } - } - } - - if (target.len >= 7 and target.len <= 40 and isHexLowerOrUpper(target)) { - if (target.len == 40) { - commit_hex = target; - } else { - commit_hex_owned = try resolveCommitPrefix(allocator, cfg.worker_base, target); - commit_hex = commit_hex_owned.?; - } - - const decoded = crypto.decodeHex(allocator, commit_hex) catch { - commit_hex = ""; - commit_hex_owned = null; - return error.InvalidCommitId; - }; - if (decoded.len != 20) { - allocator.free(decoded); - commit_hex = ""; - commit_hex_owned = null; - } else { - commit_bytes = decoded; - commit_bytes_allocated = true; - } - } - - var job_name = blk: { - if (job_name_override) |n| break :blk n; - break :blk "requeue"; - }; - - if (commit_hex.len == 0) { - const manifest_path = try manifest.resolvePathWithBase(allocator, target, cfg.worker_base); - defer allocator.free(manifest_path); - - const data = try manifest.readFileAlloc(allocator, manifest_path); - defer allocator.free(data); - - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, data, .{}); - defer parsed.deinit(); - - if (parsed.value != .object) return error.InvalidManifest; - const root = parsed.value.object; - - commit_hex = json.getString(root, "commit_id") orelse ""; - if (commit_hex.len != 40) { - colors.printError("run manifest missing commit_id\n", .{}); - return error.InvalidManifest; - } - - if (job_name_override == null) { - const j = json.getString(root, "job_name") orelse ""; - if (j.len > 0) job_name = j; - } - - const b = try crypto.decodeHex(allocator, commit_hex); - if (b.len != 20) { - allocator.free(b); - return error.InvalidCommitId; - } - commit_bytes = b; - commit_bytes_allocated = true; - } - - // Build tracking JSON with narrative/config inheritance and overrides - const tracking_json = blk: { - if (inherit_narrative or inherit_config or parent_link or overrides.items.len > 0) { - var buf = std.ArrayList(u8).empty; - defer buf.deinit(allocator); - const writer = buf.writer(allocator); - - try writer.writeAll("{"); - var first = true; - - // Add narrative if inherited - if (original_narrative) |narr| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"narrative\":"); - try writeJSONValue(writer, std.json.Value{ .object = narr }); - } - - // Add parent relationship - if (parent_run_id) |pid| { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"parent_run\":\""); - try writer.writeAll(pid); - try writer.writeAll("\""); - } - - // Add overrides - if (overrides.items.len > 0) { - if (!first) try writer.writeAll(","); - first = false; - try writer.writeAll("\"overrides\":{"); - for (overrides.items, 0..) |pair, idx| { - if (idx > 0) try writer.writeAll(","); - try writer.print("\"{s}\":\"{s}\"", .{ pair[0], pair[1] }); - } - try writer.writeAll("}"); - } - - try writer.writeAll("}"); - break :blk try buf.toOwnedSlice(allocator); - } - break :blk ""; - }; - defer if (tracking_json.len > 0) allocator.free(tracking_json); - - const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key); - defer allocator.free(api_key_hash); - - const ws_url = try cfg.getWebSocketUrl(allocator); - defer allocator.free(ws_url); - - var client = try ws.Client.connect(allocator, ws_url, cfg.api_key); - defer client.close(); - - // Send with tracking JSON if we have inheritance/overrides - if (tracking_json.len > 0) { - try client.sendQueueJobWithTrackingAndResources( - job_name, - commit_bytes, - priority, - api_key_hash, - tracking_json, - cpu, - memory, - gpu, - gpu_memory, - ); - } else if (note_final.len > 0) { - try client.sendQueueJobWithArgsNoteAndResources( - job_name, - commit_bytes, - priority, - api_key_hash, - args_final, - note_final, - force, - cpu, - memory, - gpu, - gpu_memory, - ); - } else { - try client.sendQueueJobWithArgsAndResources( - job_name, - commit_bytes, - priority, - api_key_hash, - args_final, - force, - cpu, - memory, - gpu, - gpu_memory, - ); - } - - // Receive response with duplicate detection - const message = try client.receiveMessage(allocator); - defer allocator.free(message); - - const packet = protocol.ResponsePacket.deserialize(message, allocator) catch { - if (message.len > 0 and message[0] == '{') { - try handleDuplicateResponse(allocator, message, job_name, commit_hex); - } else { - colors.printInfo("Server response: {s}\n", .{message}); - } - return; - }; - defer packet.deinit(allocator); - - switch (packet.packet_type) { - .success => { - colors.printSuccess("Queued requeue\n", .{}); - colors.printInfo("Job: {s}\n", .{job_name}); - colors.printInfo("Commit: {s}\n", .{commit_hex}); - }, - .data => { - if (packet.data_payload) |payload| { - try handleDuplicateResponse(allocator, payload, job_name, commit_hex); - } - }, - .error_packet => { - const err_msg = packet.error_message orelse "Unknown error"; - colors.printError("Error: {s}\n", .{err_msg}); - return error.ServerError; - }, - else => { - try client.handleResponsePacket(packet, "Requeue"); - colors.printSuccess("Queued requeue\n", .{}); - colors.printInfo("Job: {s}\n", .{job_name}); - colors.printInfo("Commit: {s}\n", .{commit_hex}); - }, - } -} - -fn handleDuplicateResponse( - allocator: std.mem.Allocator, - payload: []const u8, - job_name: []const u8, - commit_hex: []const u8, -) !void { - const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch { - colors.printInfo("Server response: {s}\n", .{payload}); - return; - }; - defer parsed.deinit(); - - const root = parsed.value.object; - const is_dup = root.get("duplicate") != null and root.get("duplicate").?.bool; - if (!is_dup) { - colors.printSuccess("Queued requeue\n", .{}); - colors.printInfo("Job: {s}\n", .{job_name}); - colors.printInfo("Commit: {s}\n", .{commit_hex}); - return; - } - - const existing_id = root.get("existing_id").?.string; - const status = root.get("status").?.string; - - if (std.mem.eql(u8, status, "queued") or std.mem.eql(u8, status, "running")) { - colors.printInfo("\n→ Identical job already in progress: {s}\n", .{existing_id[0..8]}); - colors.printInfo("\n Watch: ml watch {s}\n", .{existing_id[0..8]}); - } else if (std.mem.eql(u8, status, "completed")) { - colors.printInfo("\n→ Identical job already completed: {s}\n", .{existing_id[0..8]}); - colors.printInfo("\n Inspect: ml experiment show {s}\n", .{existing_id[0..8]}); - colors.printInfo(" Rerun: ml requeue {s} --force\n", .{commit_hex}); - } else if (std.mem.eql(u8, status, "failed")) { - colors.printWarning("\n→ Identical job previously failed: {s}\n", .{existing_id[0..8]}); - } -} - -fn printUsage() !void { - colors.printInfo("Usage:\n", .{}); - colors.printInfo(" ml requeue [options] -- \n\n", .{}); - colors.printInfo("Resource Options:\n", .{}); - colors.printInfo(" --name Override job name\n", .{}); - colors.printInfo(" --priority Set priority (0-255)\n", .{}); - colors.printInfo(" --cpu CPU cores\n", .{}); - colors.printInfo(" --memory Memory in GB\n", .{}); - colors.printInfo(" --gpu GPU count\n", .{}); - colors.printInfo(" --gpu-memory GPU memory budget\n", .{}); - colors.printInfo("\nInheritance Options:\n", .{}); - colors.printInfo(" --inherit-narrative Copy hypothesis/context/intent from parent\n", .{}); - colors.printInfo(" --inherit-config Copy metadata/config from parent\n", .{}); - colors.printInfo(" --parent Link as child run\n", .{}); - colors.printInfo("\nOverride Options:\n", .{}); - colors.printInfo(" --key=value Override specific config (e.g., --lr=0.002)\n", .{}); - colors.printInfo(" --args Override runner args\n", .{}); - colors.printInfo(" --note Add human note\n", .{}); - colors.printInfo("\nOther:\n", .{}); - colors.printInfo(" --force Requeue even if duplicate exists\n", .{}); - colors.printInfo(" --help, -h Show this help\n", .{}); - colors.printInfo("\nExamples:\n", .{}); - colors.printInfo(" ml requeue run_abc --lr=0.002 --batch-size=128\n", .{}); - colors.printInfo(" ml requeue run_abc --inherit-narrative --parent\n", .{}); - colors.printInfo(" ml requeue run_abc --lr=0.002 --inherit-narrative\n", .{}); -} - -fn writeJSONValue(writer: anytype, v: std.json.Value) !void { - switch (v) { - .null => try writer.writeAll("null"), - .bool => |b| try writer.print("{}", .{b}), - .integer => |i| try writer.print("{d}", .{i}), - .float => |f| try writer.print("{d}", .{f}), - .string => |s| { - try writer.writeAll("\""); - try writeEscapedString(writer, s); - try writer.writeAll("\""); - }, - .array => |arr| { - try writer.writeAll("["); - for (arr.items, 0..) |item, idx| { - if (idx > 0) try writer.writeAll(","); - try writeJSONValue(writer, item); - } - try writer.writeAll("]"); - }, - .object => |obj| { - try writer.writeAll("{"); - var first = true; - var it = obj.iterator(); - while (it.next()) |entry| { - if (!first) try writer.writeAll(","); - first = false; - try writer.print("\"{s}\":", .{entry.key_ptr.*}); - try writeJSONValue(writer, entry.value_ptr.*); - } - try writer.writeAll("}"); - }, - .number_string => |s| try writer.print("{s}", .{s}), - } -} - -fn writeEscapedString(writer: anytype, s: []const u8) !void { - for (s) |c| { - switch (c) { - '"' => try writer.writeAll("\\\""), - '\\' => try writer.writeAll("\\\\"), - '\n' => try writer.writeAll("\\n"), - '\r' => try writer.writeAll("\\r"), - '\t' => try writer.writeAll("\\t"), - else => { - if (c < 0x20) { - try writer.print("\\u00{x:0>2}", .{c}); - } else { - try writer.writeAll(&[_]u8{c}); - } - }, - } - } -} - -fn isHexLowerOrUpper(s: []const u8) bool { - for (s) |c| { - if (!std.ascii.isHex(c)) return false; - } - return true; -} - -fn resolveCommitPrefix(allocator: std.mem.Allocator, base_path: []const u8, prefix: []const u8) ![]u8 { - var dir = if (std.fs.path.isAbsolute(base_path)) - try std.fs.openDirAbsolute(base_path, .{ .iterate = true }) - else - try std.fs.cwd().openDir(base_path, .{ .iterate = true }); - defer dir.close(); - - var it = dir.iterate(); - var found: ?[]u8 = null; - errdefer if (found) |s| allocator.free(s); - - while (try it.next()) |entry| { - if (entry.kind != .directory) continue; - const name = entry.name; - if (name.len != 40) continue; - if (!std.mem.startsWith(u8, name, prefix)) continue; - if (!isHexLowerOrUpper(name)) continue; - - if (found != null) { - colors.printError("Ambiguous commit prefix: {s}\n", .{prefix}); - return error.InvalidCommitId; - } - found = try allocator.dupe(u8, name); - } - - if (found) |s| return s; - colors.printError("No commit matches prefix: {s}\n", .{prefix}); - return error.FileNotFound; -} diff --git a/cli/src/main.zig b/cli/src/main.zig index 50df8fe..703b9b2 100644 --- a/cli/src/main.zig +++ b/cli/src/main.zig @@ -12,15 +12,14 @@ pub fn main() !void { // Initialize colors based on environment colors.initColors(); - // Use ArenaAllocator for thread-safe memory management - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); + // Use c_allocator for better performance on Linux + const allocator = std.heap.c_allocator; const args = std.process.argsAlloc(allocator) catch |err| { std.debug.print("Failed to allocate args: {}\n", .{err}); return; }; + defer std.process.argsFree(allocator, args); if (args.len < 2) { printUsage(); @@ -33,61 +32,50 @@ pub fn main() !void { switch (command[0]) { 'j' => if (std.mem.eql(u8, command, "jupyter")) { try @import("commands/jupyter.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'i' => if (std.mem.eql(u8, command, "init")) { try @import("commands/init.zig").run(allocator, args[2..]); } else if (std.mem.eql(u8, command, "info")) { try @import("commands/info.zig").run(allocator, args[2..]); } else handleUnknownCommand(command), - 'a' => if (std.mem.eql(u8, command, "annotate")) { - try @import("commands/annotate.zig").run(allocator, args[2..]); - }, - 'n' => if (std.mem.eql(u8, command, "narrative")) { - try @import("commands/narrative.zig").run(allocator, args[2..]); - } else if (std.mem.eql(u8, command, "outcome")) { - try @import("commands/outcome.zig").run(allocator, args[2..]); - }, - 'p' => if (std.mem.eql(u8, command, "privacy")) { - try @import("commands/privacy.zig").run(allocator, args[2..]); - }, + 'n' => if (std.mem.eql(u8, command, "note")) { + try @import("commands/note.zig").run(allocator, args[2..]); + } else handleUnknownCommand(command), 's' => if (std.mem.eql(u8, command, "sync")) { - if (args.len < 3) { - colors.printError("Usage: ml sync \n", .{}); - return error.InvalidArgs; - } - colors.printInfo("Sync project to server: {s}\n", .{args[2]}); + try @import("commands/sync.zig").run(allocator, args[2..]); } else if (std.mem.eql(u8, command, "status")) { try @import("commands/status.zig").run(allocator, args[2..]); } else handleUnknownCommand(command), - 'r' => if (std.mem.eql(u8, command, "requeue")) { - try @import("commands/requeue.zig").run(allocator, args[2..]); - } else if (std.mem.eql(u8, command, "run")) { + 'r' => if (std.mem.eql(u8, command, "run")) { try @import("commands/run.zig").execute(allocator, args[2..]); } else handleUnknownCommand(command), 'q' => if (std.mem.eql(u8, command, "queue")) { try @import("commands/queue.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'd' => if (std.mem.eql(u8, command, "dataset")) { try @import("commands/dataset.zig").run(allocator, args[2..]); - }, - 'e' => if (std.mem.eql(u8, command, "experiment")) { - try @import("commands/experiment.zig").execute(allocator, args[2..]); - } else if (std.mem.eql(u8, command, "export")) { + } else handleUnknownCommand(command), + 'e' => if (std.mem.eql(u8, command, "export")) { try @import("commands/export_cmd.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'c' => if (std.mem.eql(u8, command, "cancel")) { try @import("commands/cancel.zig").run(allocator, args[2..]); } else if (std.mem.eql(u8, command, "compare")) { try @import("commands/compare.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'f' => if (std.mem.eql(u8, command, "find")) { try @import("commands/find.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'v' => if (std.mem.eql(u8, command, "validate")) { try @import("commands/validate.zig").run(allocator, args[2..]); - }, + } else handleUnknownCommand(command), 'l' => if (std.mem.eql(u8, command, "logs")) { - try @import("commands/logs.zig").run(allocator, args[2..]); + try @import("commands/log.zig").run(allocator, args[2..]); + } else if (std.mem.eql(u8, command, "log")) { + try @import("commands/log.zig").run(allocator, args[2..]); + } else handleUnknownCommand(command), + 'w' => if (std.mem.eql(u8, command, "watch")) { + try @import("commands/watch.zig").run(allocator, args[2..]); } else handleUnknownCommand(command), else => { colors.printError("Unknown command: {s}\n", .{args[1]}); @@ -102,44 +90,30 @@ fn printUsage() void { colors.printInfo("ML Experiment Manager\n\n", .{}); std.debug.print("Usage: ml [options]\n\n", .{}); std.debug.print("Commands:\n", .{}); - std.debug.print(" jupyter Jupyter workspace management\n", .{}); - std.debug.print(" init Initialize local experiment tracking database\n", .{}); - std.debug.print(" experiment Manage experiments (auto-detects local/server mode)\n", .{}); - std.debug.print(" - create, list, log, show, delete\n", .{}); - std.debug.print(" run Manage runs (auto-detects local/server mode)\n", .{}); - std.debug.print(" - start, finish, fail, list\n", .{}); - std.debug.print(" annotate Add an annotation to run_manifest.json (--note \"...\")\n", .{}); - std.debug.print(" compare Compare two runs (show differences)\n", .{}); - std.debug.print(" export Export experiment bundle (--anonymize for safe sharing)\n", .{}); - std.debug.print(" find [query] Search experiments by tags/outcome/dataset\n", .{}); - std.debug.print(" narrative set Set run narrative fields (hypothesis/context/...)\n", .{}); - std.debug.print(" outcome set Set post-run outcome (validates/refutes/inconclusive)\n", .{}); - std.debug.print(" privacy set Set experiment privacy level (private/team/public)\n", .{}); - std.debug.print(" info Show run info from run_manifest.json (optionally --base )\n", .{}); - std.debug.print(" sync Sync project to server\n", .{}); - std.debug.print(" requeue Re-submit from run_id/task_id/path (supports -- )\n", .{}); - std.debug.print(" queue (q) Queue job for execution\n", .{}); + std.debug.print(" init Initialize project with config (use --local for SQLite)\n", .{}); + std.debug.print(" run [args] Execute a run locally (forks, captures, parses metrics)\n", .{}); + std.debug.print(" queue Queue job on server (--rerun to re-queue local run)\n", .{}); + std.debug.print(" note Add metadata annotation (hypothesis/outcome/confidence)\n", .{}); + std.debug.print(" logs Fetch or stream run logs (--follow for live tail)\n", .{}); + std.debug.print(" sync [id] Push local runs to server (sync_run + sync_ack protocol)\n", .{}); + std.debug.print(" cancel Cancel local run (SIGTERM/SIGKILL by PID)\n", .{}); + std.debug.print(" watch [--sync] Watch directory with optional auto-sync\n", .{}); std.debug.print(" status Get system status\n", .{}); - std.debug.print(" monitor Launch TUI via SSH\n", .{}); - std.debug.print(" logs Fetch job logs (-f to follow, -n for tail)\n", .{}); - std.debug.print(" cancel Cancel running job\n", .{}); - std.debug.print(" prune Remove old experiments\n", .{}); - std.debug.print(" watch Watch directory for auto-sync\n", .{}); std.debug.print(" dataset Manage datasets\n", .{}); - std.debug.print(" experiment Manage experiments and metrics\n", .{}); - std.debug.print(" validate Validate provenance and integrity for a commit/task\n", .{}); + std.debug.print(" export Export experiment bundle\n", .{}); + std.debug.print(" validate Validate provenance and integrity\n", .{}); + std.debug.print(" compare Compare two runs\n", .{}); + std.debug.print(" find [query] Search experiments\n", .{}); + std.debug.print(" jupyter Jupyter workspace management\n", .{}); + std.debug.print(" info Show run info\n", .{}); std.debug.print("\nUse 'ml --help' for detailed help.\n", .{}); } test { _ = @import("commands/info.zig"); - _ = @import("commands/requeue.zig"); - _ = @import("commands/annotate.zig"); - _ = @import("commands/narrative.zig"); - _ = @import("commands/outcome.zig"); - _ = @import("commands/privacy.zig"); _ = @import("commands/compare.zig"); _ = @import("commands/find.zig"); _ = @import("commands/export_cmd.zig"); - _ = @import("commands/logs.zig"); + _ = @import("commands/log.zig"); + _ = @import("commands/note.zig"); }