@controlvector/cv-agent 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # CV-Agent
2
+
3
+ **Remote task dispatch daemon for CV-Hub. Bridges Claude Code with CV-Hub's agentic task system.**
4
+
5
+ CV-Agent (`cva`) runs as a daemon on your machine, polls CV-Hub for dispatched tasks, spawns Claude Code to execute them, and reports results back. It handles permission relay, git remote management, and executor heartbeats.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@controlvector/cv-agent)](https://www.npmjs.com/package/@controlvector/cv-agent)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -g @controlvector/cv-agent
16
+ ```
17
+
18
+ Requires [Claude Code](https://docs.anthropic.com/en/docs/build-with-claude/claude-code) to be installed and on your PATH.
19
+
20
+ ---
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ # Authenticate with CV-Hub
26
+ cva auth login
27
+
28
+ # Add the CV-Hub remote to your repo
29
+ cd your-project
30
+ cva remote add
31
+
32
+ # Start the agent daemon (auto-approves Claude Code tool permissions)
33
+ cva agent --auto-approve
34
+ ```
35
+
36
+ The agent will register as an executor with CV-Hub, poll for tasks, and execute them using Claude Code.
37
+
38
+ ---
39
+
40
+ ## Commands
41
+
42
+ ### `cva agent [options]`
43
+
44
+ Start the agent daemon. Polls CV-Hub for pending tasks and executes them with Claude Code.
45
+
46
+ ```bash
47
+ cva agent # Start with permission relay to CV-Hub
48
+ cva agent --auto-approve # Auto-approve all Claude Code permissions locally
49
+ cva agent --dir /path # Override working directory
50
+ ```
51
+
52
+ Options:
53
+ - `--auto-approve` — Auto-approve all tool permission prompts (no CV-Hub relay)
54
+ - `--dir <path>` — Working directory for Claude Code sessions
55
+
56
+ ### `cva auth login`
57
+
58
+ Authenticate with CV-Hub. Opens a browser for device auth flow, or accepts a token paste.
59
+
60
+ ```bash
61
+ cva auth login
62
+ ```
63
+
64
+ Token is stored in `~/.cva/config.json`.
65
+
66
+ ### `cva remote add [--name <n>]`
67
+
68
+ Add or update the CV-Hub git remote for the current repository.
69
+
70
+ ```bash
71
+ cva remote add # Auto-detects from CV-Hub
72
+ cva remote add --name cvhub # Custom remote name (default: cvhub)
73
+ ```
74
+
75
+ ### `cva remote setup <owner/repo>`
76
+
77
+ Create a repo on CV-Hub and configure the local remote in one step.
78
+
79
+ ### `cva task list [--status <status>]`
80
+
81
+ List tasks for the current executor.
82
+
83
+ ### `cva task logs <task-id>`
84
+
85
+ Stream task logs and progress.
86
+
87
+ ### `cva status`
88
+
89
+ Show executor registration status, active tasks, and recent completions.
90
+
91
+ ---
92
+
93
+ ## How It Works
94
+
95
+ 1. `cva agent` registers this machine as an **executor** with CV-Hub
96
+ 2. It polls CV-Hub every 5 seconds for pending tasks
97
+ 3. When a task is claimed, it spawns Claude Code with the task prompt
98
+ 4. **Permission handling**: Claude Code tool calls require approval:
99
+ - `--auto-approve`: writes `y` to stdin automatically
100
+ - Default: relays prompts to CV-Hub for remote approval (e.g., from Claude.ai)
101
+ 5. When Claude Code exits, the agent reports the result (files changed, commits, exit code) to CV-Hub
102
+ 6. Heartbeats are sent every 30 seconds to keep the executor registration alive
103
+
104
+ ### Git Remote Management
105
+
106
+ Before each task, the agent ensures a `cvhub` remote exists pointing to the correct CV-Hub repo URL. Task prompts are prepended with instructions to push to `cvhub` instead of `origin`.
107
+
108
+ ---
109
+
110
+ ## Prerequisites
111
+
112
+ | Requirement | Version | Notes |
113
+ |-------------|---------|-------|
114
+ | Node.js | >= 20 | Runtime |
115
+ | Claude Code | Latest | Must be on PATH as `claude` |
116
+ | CV-Hub account | — | Sign up at [hub.controlvector.io](https://hub.controlvector.io) |
117
+
118
+ ---
119
+
120
+ ## Configuration
121
+
122
+ Config file: `~/.cva/config.json`
123
+
124
+ ```json
125
+ {
126
+ "hub": {
127
+ "api": "https://api.hub.controlvector.io",
128
+ "token": "cvh_xxxxxxxxxxxx"
129
+ },
130
+ "agent": {
131
+ "pollInterval": 5000,
132
+ "heartbeatInterval": 30000,
133
+ "autoApprove": false,
134
+ "maxConcurrentTasks": 1
135
+ },
136
+ "defaults": {
137
+ "remoteName": "cvhub"
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Why a Separate Binary?
145
+
146
+ CV-Git (`cv`) and CV-Agent (`cva`) are separate packages to avoid a recursion trap: `cv agent` would spawn Claude Code, which might call `cv` commands, creating a circular dependency. By using `cva` as the agent binary, Claude Code sessions use standard `git` commands only.
147
+
148
+ | Binary | Package | Responsibility |
149
+ |--------|---------|---------------|
150
+ | `cv` | `@controlvector/cv-git` | Git operations, knowledge graph, code analysis, local dev |
151
+ | `cva` | `@controlvector/cv-agent` | Agent daemon, task dispatch, CV-Hub remote management |
152
+
153
+ ---
154
+
155
+ ## Related Projects
156
+
157
+ - [CV-Git](https://www.npmjs.com/package/@controlvector/cv-git) (`cv`) — AI-native version control CLI
158
+ - [CV-Hub](https://hub.controlvector.io) — AI-native Git platform (web app + API)
159
+
160
+ ---
161
+
162
+ ## License
163
+
164
+ MIT
package/dist/bundle.cjs CHANGED
@@ -3743,6 +3743,28 @@ async function getTaskLogs(creds, taskId) {
3743
3743
  const data = await res.json();
3744
3744
  return data.logs || [];
3745
3745
  }
3746
+ async function postTaskEvent(creds, taskId, event) {
3747
+ const res = await apiCall(creds, "POST", `/api/v1/tasks/${taskId}/events`, event);
3748
+ if (!res.ok) throw new Error(`Post event failed: ${res.status}`);
3749
+ return await res.json();
3750
+ }
3751
+ async function getEventResponse(creds, taskId, eventId) {
3752
+ const res = await apiCall(creds, "GET", `/api/v1/tasks/${taskId}/events?after_id=&limit=200`);
3753
+ if (!res.ok) throw new Error(`Get events failed: ${res.status}`);
3754
+ const events = await res.json();
3755
+ const event = events.find((e) => e.id === eventId);
3756
+ return {
3757
+ response: event?.response ?? null,
3758
+ responded_at: event?.respondedAt ?? event?.responded_at ?? null
3759
+ };
3760
+ }
3761
+ async function getRedirects(creds, taskId, afterTimestamp) {
3762
+ const qs = afterTimestamp ? `?after_timestamp=${encodeURIComponent(afterTimestamp)}` : "";
3763
+ const res = await apiCall(creds, "GET", `/api/v1/tasks/${taskId}/events${qs}`);
3764
+ if (!res.ok) return [];
3765
+ const events = await res.json();
3766
+ return events.filter((e) => (e.eventType ?? e.event_type) === "redirect").map((e) => ({ id: e.id, content: e.content }));
3767
+ }
3746
3768
  async function createRepo(creds, name, description) {
3747
3769
  const res = await apiCall(creds, "POST", "/api/v1/repos", {
3748
3770
  name,
@@ -3755,6 +3777,44 @@ async function createRepo(creds, name, description) {
3755
3777
  return await res.json();
3756
3778
  }
3757
3779
 
3780
+ // src/utils/output-parser.ts
3781
+ function parseClaudeCodeOutput(line) {
3782
+ const trimmed = line.trim();
3783
+ if (!trimmed) return null;
3784
+ const thinkingMatch = trimmed.match(/^\[THINKING\]\s*(.+)/);
3785
+ if (thinkingMatch) {
3786
+ return { eventType: "thinking", content: thinkingMatch[1], needsResponse: false };
3787
+ }
3788
+ const decisionMatch = trimmed.match(/^\[DECISION\]\s*(.+)/);
3789
+ if (decisionMatch) {
3790
+ return { eventType: "decision", content: decisionMatch[1], needsResponse: false };
3791
+ }
3792
+ const questionMatch = trimmed.match(/^\[QUESTION\]\s*(.+)/);
3793
+ if (questionMatch) {
3794
+ return { eventType: "question", content: questionMatch[1], needsResponse: true };
3795
+ }
3796
+ const progressMatch = trimmed.match(/^\[PROGRESS\]\s*(.+)/);
3797
+ if (progressMatch) {
3798
+ return { eventType: "progress", content: progressMatch[1], needsResponse: false };
3799
+ }
3800
+ const fileChangeMatch = trimmed.match(
3801
+ /(?:Created|Modified|Wrote to|Deleted)\s+(?:file:\s*)?(.+)/i
3802
+ );
3803
+ if (fileChangeMatch) {
3804
+ const action = trimmed.split(/\s/)[0].toLowerCase();
3805
+ return {
3806
+ eventType: "file_change",
3807
+ content: { path: fileChangeMatch[1].trim(), action },
3808
+ needsResponse: false
3809
+ };
3810
+ }
3811
+ const errorMatch = trimmed.match(/^(?:Error|FATAL|FAILED|panic|Traceback)/i);
3812
+ if (errorMatch) {
3813
+ return { eventType: "error", content: trimmed, needsResponse: false };
3814
+ }
3815
+ return null;
3816
+ }
3817
+
3758
3818
  // src/commands/agent-git.ts
3759
3819
  var import_node_child_process = require("node:child_process");
3760
3820
  function gitExec(cmd, cwd) {
@@ -3996,7 +4056,7 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
3996
4056
  // src/commands/agent.ts
3997
4057
  function buildClaudePrompt(task) {
3998
4058
  let prompt = "";
3999
- prompt += `You are executing a task dispatched from Claude.ai via CV-Hub.
4059
+ prompt += `You are executing a task dispatched via CV-Hub.
4000
4060
 
4001
4061
  `;
4002
4062
  prompt += `## Task: ${task.title}
@@ -4145,33 +4205,84 @@ var PERMISSION_PATTERNS = [
4145
4205
  /\? \(y\/n\)/
4146
4206
  ];
4147
4207
  async function launchAutoApproveMode(prompt, options) {
4148
- return new Promise((resolve, reject) => {
4149
- const args = ["-p", prompt, "--allowedTools", ...ALLOWED_TOOLS];
4150
- const child = (0, import_node_child_process2.spawn)("claude", args, {
4151
- cwd: options.cwd,
4152
- stdio: ["inherit", "inherit", "pipe"],
4153
- env: { ...process.env }
4154
- });
4155
- _activeChild = child;
4156
- let stderr = "";
4157
- child.stderr?.on("data", (data) => {
4158
- const text = data.toString();
4159
- stderr += text;
4160
- process.stderr.write(data);
4208
+ const sessionId = options.taskId ? options.taskId.replace(/-/g, "").slice(0, 32).padEnd(32, "0").replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5") : void 0;
4209
+ const pendingQuestionIds = [];
4210
+ const runOnce = (inputPrompt, isContinue) => {
4211
+ return new Promise((resolve, reject) => {
4212
+ const args = isContinue ? ["-p", inputPrompt, "--continue", "--allowedTools", ...ALLOWED_TOOLS] : ["-p", inputPrompt, "--allowedTools", ...ALLOWED_TOOLS];
4213
+ if (sessionId && !isContinue) {
4214
+ args.push("--session-id", sessionId);
4215
+ }
4216
+ const child = (0, import_node_child_process2.spawn)("claude", args, {
4217
+ cwd: options.cwd,
4218
+ stdio: ["inherit", "pipe", "pipe"],
4219
+ env: { ...process.env }
4220
+ });
4221
+ _activeChild = child;
4222
+ let stderr = "";
4223
+ let lineBuffer = "";
4224
+ child.stdout?.on("data", (data) => {
4225
+ const text = data.toString();
4226
+ process.stdout.write(data);
4227
+ if (options.creds && options.taskId) {
4228
+ lineBuffer += text;
4229
+ const lines = lineBuffer.split("\n");
4230
+ lineBuffer = lines.pop() ?? "";
4231
+ for (const line of lines) {
4232
+ const event = parseClaudeCodeOutput(line);
4233
+ if (event) {
4234
+ postTaskEvent(options.creds, options.taskId, {
4235
+ event_type: event.eventType,
4236
+ content: event.content,
4237
+ needs_response: event.needsResponse
4238
+ }).then((created) => {
4239
+ if (event.needsResponse && created?.id) {
4240
+ pendingQuestionIds.push(created.id);
4241
+ }
4242
+ }).catch(() => {
4243
+ });
4244
+ }
4245
+ }
4246
+ }
4247
+ });
4248
+ child.stderr?.on("data", (data) => {
4249
+ stderr += data.toString();
4250
+ process.stderr.write(data);
4251
+ });
4252
+ child.on("close", (code, signal) => {
4253
+ _activeChild = null;
4254
+ resolve({ exitCode: signal === "SIGKILL" ? 137 : code ?? 1, stderr });
4255
+ });
4256
+ child.on("error", (err) => {
4257
+ _activeChild = null;
4258
+ reject(err);
4259
+ });
4161
4260
  });
4162
- child.on("close", (code, signal) => {
4163
- _activeChild = null;
4164
- if (signal === "SIGKILL") {
4165
- resolve({ exitCode: 137, stderr });
4261
+ };
4262
+ let result = await runOnce(prompt, false);
4263
+ if (options.creds && options.taskId && result.exitCode === 0) {
4264
+ let followUps = 0;
4265
+ while (pendingQuestionIds.length > 0 && followUps < 3) {
4266
+ const questionId = pendingQuestionIds.shift();
4267
+ console.log(source_default.gray(` [auto-approve] Waiting for planner response to question...`));
4268
+ const response = await pollForEventResponse(
4269
+ options.creds,
4270
+ options.taskId,
4271
+ questionId,
4272
+ 3e5
4273
+ );
4274
+ if (response) {
4275
+ const responseText = typeof response === "string" ? response : JSON.stringify(response);
4276
+ console.log(source_default.gray(` [auto-approve] Planner responded, continuing with --continue`));
4277
+ result = await runOnce(responseText, true);
4278
+ followUps++;
4166
4279
  } else {
4167
- resolve({ exitCode: code ?? 1, stderr });
4280
+ console.log(source_default.yellow(` [auto-approve] No response received, continuing without.`));
4281
+ break;
4168
4282
  }
4169
- });
4170
- child.on("error", (err) => {
4171
- _activeChild = null;
4172
- reject(err);
4173
- });
4174
- });
4283
+ }
4284
+ }
4285
+ return result;
4175
4286
  }
4176
4287
  async function launchRelayMode(prompt, options) {
4177
4288
  return new Promise((resolve, reject) => {
@@ -4184,10 +4295,65 @@ async function launchRelayMode(prompt, options) {
4184
4295
  let stderr = "";
4185
4296
  let stdoutBuffer = "";
4186
4297
  child.stdin?.write(prompt + "\n");
4298
+ let lastRedirectCheck = Date.now();
4299
+ let lineBuffer = "";
4187
4300
  child.stdout?.on("data", async (data) => {
4188
4301
  const text = data.toString();
4189
4302
  process.stdout.write(data);
4190
4303
  stdoutBuffer += text;
4304
+ lineBuffer += text;
4305
+ const lines = lineBuffer.split("\n");
4306
+ lineBuffer = lines.pop() ?? "";
4307
+ for (const line of lines) {
4308
+ const event = parseClaudeCodeOutput(line);
4309
+ if (event) {
4310
+ try {
4311
+ const created = await postTaskEvent(options.creds, options.taskId, {
4312
+ event_type: event.eventType,
4313
+ content: event.content,
4314
+ needs_response: event.needsResponse
4315
+ });
4316
+ if (event.needsResponse && created?.id) {
4317
+ console.log(source_default.gray(` [stream] Question detected, waiting for planner response...`));
4318
+ const response = await pollForEventResponse(
4319
+ options.creds,
4320
+ options.taskId,
4321
+ created.id,
4322
+ 3e5
4323
+ );
4324
+ if (response) {
4325
+ const responseText = typeof response === "string" ? response : JSON.stringify(response);
4326
+ child.stdin?.write(responseText + "\n");
4327
+ console.log(source_default.gray(` [stream] Planner responded.`));
4328
+ } else {
4329
+ child.stdin?.write("[No response received within timeout. Continue with your best judgment.]\n");
4330
+ console.log(source_default.yellow(` [stream] Question timed out.`));
4331
+ }
4332
+ }
4333
+ } catch {
4334
+ }
4335
+ }
4336
+ }
4337
+ if (Date.now() - lastRedirectCheck > 1e4) {
4338
+ try {
4339
+ const redirects = await getRedirects(
4340
+ options.creds,
4341
+ options.taskId,
4342
+ new Date(lastRedirectCheck).toISOString()
4343
+ );
4344
+ for (const redirect of redirects) {
4345
+ const instruction = redirect.content?.instruction;
4346
+ if (instruction) {
4347
+ child.stdin?.write(`
4348
+ [REDIRECT FROM PLANNER]: ${instruction}
4349
+ `);
4350
+ console.log(source_default.gray(` [stream] Redirect received from planner.`));
4351
+ }
4352
+ }
4353
+ } catch {
4354
+ }
4355
+ lastRedirectCheck = Date.now();
4356
+ }
4191
4357
  for (const pattern of PERMISSION_PATTERNS) {
4192
4358
  const match = stdoutBuffer.match(pattern);
4193
4359
  if (match) {
@@ -4210,6 +4376,12 @@ async function launchRelayMode(prompt, options) {
4210
4376
  "approval",
4211
4377
  ["y", "n"]
4212
4378
  );
4379
+ postTaskEvent(options.creds, options.taskId, {
4380
+ event_type: "approval_request",
4381
+ content: { prompt_text: promptText },
4382
+ needs_response: true
4383
+ }).catch(() => {
4384
+ });
4213
4385
  const timeoutMs = 5 * 60 * 1e3;
4214
4386
  const startPoll = Date.now();
4215
4387
  let answered = false;
@@ -4266,6 +4438,20 @@ async function launchRelayMode(prompt, options) {
4266
4438
  });
4267
4439
  });
4268
4440
  }
4441
+ async function pollForEventResponse(creds, taskId, eventId, timeoutMs) {
4442
+ const start = Date.now();
4443
+ while (Date.now() - start < timeoutMs) {
4444
+ await new Promise((r) => setTimeout(r, 3e3));
4445
+ try {
4446
+ const result = await getEventResponse(creds, taskId, eventId);
4447
+ if (result.response !== null) {
4448
+ return result.response;
4449
+ }
4450
+ } catch {
4451
+ }
4452
+ }
4453
+ return null;
4454
+ }
4269
4455
  async function runAgent(options) {
4270
4456
  const creds = await readCredentials();
4271
4457
  if (!creds.CV_HUB_API) {
@@ -4300,7 +4486,7 @@ async function runAgent(options) {
4300
4486
  }
4301
4487
  const machineName = options.machine || await getMachineName();
4302
4488
  const pollInterval = Math.max(3, parseInt(options.pollInterval, 10)) * 1e3;
4303
- const workingDir = options.workingDir;
4489
+ const workingDir = options.workingDir === "." ? process.cwd() : options.workingDir;
4304
4490
  if (!options.machine) {
4305
4491
  const credCheck = await readCredentials();
4306
4492
  if (!credCheck.CV_HUB_MACHINE_NAME) {
@@ -4420,7 +4606,11 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4420
4606
  console.log(source_default.gray("-".repeat(60)));
4421
4607
  let result;
4422
4608
  if (options.autoApprove) {
4423
- result = await launchAutoApproveMode(prompt, { cwd: options.workingDir });
4609
+ result = await launchAutoApproveMode(prompt, {
4610
+ cwd: options.workingDir,
4611
+ creds,
4612
+ taskId: task.id
4613
+ });
4424
4614
  } else {
4425
4615
  result = await launchRelayMode(prompt, {
4426
4616
  cwd: options.workingDir,
@@ -4438,6 +4628,15 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4438
4628
  ...postGitState.filesModified,
4439
4629
  ...postGitState.filesDeleted
4440
4630
  ];
4631
+ postTaskEvent(creds, task.id, {
4632
+ event_type: "completed",
4633
+ content: {
4634
+ exit_code: result.exitCode,
4635
+ duration_seconds: Math.round((Date.now() - startTime) / 1e3),
4636
+ files_changed: allChangedFiles.length
4637
+ }
4638
+ }).catch(() => {
4639
+ });
4441
4640
  if (result.exitCode === 0) {
4442
4641
  if (allChangedFiles.length > 0) {
4443
4642
  sendTaskLog(
@@ -4530,10 +4729,10 @@ ${source_default.red("!")} Task error: ${err.message}`);
4530
4729
  }
4531
4730
  function agentCommand() {
4532
4731
  const cmd = new Command("agent");
4533
- cmd.description("Listen for tasks dispatched from Claude.ai and execute them with Claude Code");
4534
- cmd.option("--machine <name>", "Machine name override");
4535
- cmd.option("--poll-interval <seconds>", "How often to check for tasks", "5");
4536
- cmd.option("--working-dir <path>", "Working directory for Claude Code", process.cwd());
4732
+ cmd.description("Listen for tasks dispatched via CV-Hub and execute them with Claude Code");
4733
+ cmd.option("--machine <name>", "Override auto-detected machine name");
4734
+ cmd.option("--poll-interval <seconds>", "How often to check for tasks, minimum 3 (default: 5)", "5");
4735
+ cmd.option("--working-dir <path>", "Working directory for Claude Code (default: current directory)", ".");
4537
4736
  cmd.option("--auto-approve", "Pre-approve all tool permissions (uses -p mode)", false);
4538
4737
  cmd.action(async (opts) => {
4539
4738
  await runAgent(opts);
@@ -4547,7 +4746,7 @@ async function authLogin(opts) {
4547
4746
  const apiUrl = opts.apiUrl || "https://api.hub.controlvector.io";
4548
4747
  console.log(source_default.gray("Validating token..."));
4549
4748
  try {
4550
- const res = await fetch(`${apiUrl}/api/v1/user/me`, {
4749
+ const res = await fetch(`${apiUrl}/api/auth/me`, {
4551
4750
  headers: { "Authorization": `Bearer ${token}` }
4552
4751
  });
4553
4752
  if (!res.ok) {
@@ -4594,7 +4793,7 @@ async function authStatus() {
4594
4793
  console.log(`API: ${source_default.cyan(apiUrl)}`);
4595
4794
  console.log(`Token: ${source_default.gray(maskedToken)}`);
4596
4795
  try {
4597
- const res = await fetch(`${apiUrl}/api/v1/user/me`, {
4796
+ const res = await fetch(`${apiUrl}/api/auth/me`, {
4598
4797
  headers: { "Authorization": `Bearer ${creds.CV_HUB_PAT}` }
4599
4798
  });
4600
4799
  if (res.ok) {
@@ -4834,7 +5033,7 @@ function statusCommand() {
4834
5033
 
4835
5034
  // src/index.ts
4836
5035
  var program2 = new Command();
4837
- program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("0.1.0");
5036
+ program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("1.0.0");
4838
5037
  program2.addCommand(agentCommand());
4839
5038
  program2.addCommand(authCommand());
4840
5039
  program2.addCommand(remoteCommand());