chorus-cli 0.4.2 → 0.4.4

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/index.js CHANGED
@@ -18,6 +18,43 @@ const execPromise = util.promisify(exec);
18
18
  const execFilePromise = util.promisify(execFile);
19
19
  const fs = require('fs').promises;
20
20
 
21
+ // Returns a stable hardware UUID for this machine, with a persistent fallback
22
+ async function getMachineId() {
23
+ try {
24
+ if (process.platform === 'darwin') {
25
+ const { stdout } = await execPromise(
26
+ "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}'"
27
+ );
28
+ if (stdout.trim()) return stdout.trim();
29
+ } else if (process.platform === 'linux') {
30
+ const fsp = require('fs').promises;
31
+ const id = (await fsp.readFile('/etc/machine-id', 'utf8')).trim();
32
+ if (id) return id;
33
+ } else if (process.platform === 'win32') {
34
+ const { stdout } = await execPromise('wmic csproduct get UUID');
35
+ const lines = stdout.trim().split('\n');
36
+ if (lines.length > 1) {
37
+ const uuid = lines[1].trim();
38
+ if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF') return uuid;
39
+ }
40
+ }
41
+ } catch { /* fall through to persistent fallback */ }
42
+
43
+ // Persistent fallback: generate and cache a random UUID
44
+ const configDir = path.join(os.homedir(), '.config', 'chorus');
45
+ const idPath = path.join(configDir, 'machine-id');
46
+ try {
47
+ const existing = await fs.readFile(idPath, 'utf8');
48
+ if (existing.trim()) return existing.trim();
49
+ } catch { /* no file yet */ }
50
+
51
+ const { randomUUID } = require('crypto');
52
+ const newId = randomUUID();
53
+ await fs.mkdir(configDir, { recursive: true });
54
+ await fs.writeFile(idPath, newId + '\n');
55
+ return newId;
56
+ }
57
+
21
58
  // Run coder.py with real-time stderr streaming so progress is visible
22
59
  function runCoder(prompt) {
23
60
  return new Promise((resolve, reject) => {
@@ -26,6 +63,9 @@ function runCoder(prompt) {
26
63
  env.CHORUS_API_KEY = CONFIG.ai.chorusApiKey;
27
64
  env.CHORUS_API_URL = CONFIG.ai.chorusApiUrl;
28
65
  }
66
+ if (CONFIG.ai.machineId) {
67
+ env.CHORUS_MACHINE_ID = CONFIG.ai.machineId;
68
+ }
29
69
  const proc = spawn(CONFIG.ai.venvPython, [CONFIG.ai.coderPath, '--prompt', prompt], {
30
70
  cwd: process.cwd(),
31
71
  env,
@@ -77,6 +117,9 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
77
117
  env.CHORUS_API_KEY = CONFIG.ai.chorusApiKey;
78
118
  env.CHORUS_API_URL = CONFIG.ai.chorusApiUrl;
79
119
  }
120
+ if (CONFIG.ai.machineId) {
121
+ env.CHORUS_MACHINE_ID = CONFIG.ai.machineId;
122
+ }
80
123
  if (CONFIG.messenger === 'slack' && CONFIG.slack.botToken) {
81
124
  env.SLACK_BOT_TOKEN = CONFIG.slack.botToken;
82
125
  }
@@ -194,10 +237,14 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
194
237
  if (!CONFIG.ai.chorusApiKey) {
195
238
  throw new Error('CHORUS_API_KEY environment variable is required. Run "chorus setup" to configure.');
196
239
  }
197
- const openai = new OpenAI({
240
+ const openaiOpts = {
198
241
  apiKey: CONFIG.ai.chorusApiKey,
199
242
  baseURL: CONFIG.ai.chorusApiUrl,
200
- });
243
+ };
244
+ if (CONFIG.ai.machineId) {
245
+ openaiOpts.defaultHeaders = { 'X-Machine-Id': CONFIG.ai.machineId };
246
+ }
247
+ const openai = new OpenAI(openaiOpts);
201
248
 
202
249
  const response = await openai.chat.completions.create({
203
250
  model: 'anthropic/claude-opus-4',
@@ -506,6 +553,9 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
506
553
  try {
507
554
  console.log('🚀 Starting ticket processing...\n');
508
555
 
556
+ // Resolve machine fingerprint for per-machine usage tracking
557
+ CONFIG.ai.machineId = await getMachineId();
558
+
509
559
  // 0. Ensure Python venv exists and has required dependencies
510
560
  const reqFile = path.join(__dirname, 'tools', 'requirements.txt');
511
561
  const { execFileSync: efs } = require('child_process');
@@ -869,6 +919,9 @@ async function setupProxyAuth() {
869
919
 
870
920
  const baseUrl = CONFIG.ai.chorusApiUrl.replace(/\/v1\/?$/, '');
871
921
 
922
+ // Resolve machine fingerprint so the server can track per-machine usage
923
+ const machineId = await getMachineId();
924
+
872
925
  // Try register first, fall back to login if already registered
873
926
  let apiKey;
874
927
  try {
@@ -895,11 +948,11 @@ async function setupProxyAuth() {
895
948
  });
896
949
 
897
950
  console.log(' Registering...');
898
- let res = await doPost(`${baseUrl}/auth/register`, { email, password });
951
+ let res = await doPost(`${baseUrl}/auth/register`, { email, password, machineId });
899
952
 
900
953
  if (res.status === 409 || (res.body && res.body.error && res.body.error.includes('already'))) {
901
954
  console.log(' Account exists, logging in...');
902
- res = await doPost(`${baseUrl}/auth/login`, { email, password });
955
+ res = await doPost(`${baseUrl}/auth/login`, { email, password, machineId });
903
956
  }
904
957
 
905
958
  if (res.status >= 400) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chorus-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Automated ticket resolution with AI, Teams, and Slack integration",
5
5
  "main": "index.js",
6
6
  "bin": {
package/tools/coder.py CHANGED
@@ -50,23 +50,29 @@ def is_token_limit_error(err):
50
50
  return "token limit exceeded" in msg or "rate_limit_error" in msg
51
51
 
52
52
  SYSTEM_PROMPT = """\
53
- You are a coding agent running in the terminal.
53
+ You are a coding agent running inside a CLI tool called Chorus.
54
+ Your output goes straight to a terminal. There is no browser, no rich renderer.
54
55
  Working directory: {cwd}
55
56
 
56
57
  You help with software engineering tasks: writing code, debugging, refactoring, \
57
58
  explaining code, running commands, and managing files.
58
59
 
59
60
  Formatting:
60
- - Your output is displayed raw in a terminal. Never use markdown.
61
- - No ## headers, **bold**, *italic*, [links](url), or bullet symbols like -.
61
+ - Plain text only. Never use markdown.
62
+ - No ## headers, **bold**, *italic*, `backticks`, [links](url), or bullet symbols like -.
62
63
  - Use blank lines, indentation, and CAPS for emphasis or section labels.
63
64
  - Use plain numbered lists (1. 2. 3.) when listing things.
64
- - For inline code references, just use the name directly (e.g. myFunction, not `myFunction`).
65
- - Keep responses short and scannable.
65
+ - Refer to code identifiers by name directly (e.g. myFunction, not `myFunction`).
66
+
67
+ Communication style:
68
+ - Be terse. Say what you are doing and why in one or two lines, then do it.
69
+ - No greetings, preambles, encouragement, or sign-offs.
70
+ - No "Great question!", "Let me", "Sure!", "I'll now", or similar filler.
71
+ - When explaining your plan, use short declarative sentences. Skip obvious reasoning.
72
+ - After completing work, state what changed and nothing else.
66
73
 
67
74
  {approach}Guidelines:
68
- - Be direct and concise. No filler.
69
- - Always use your tools. If a question can be answered by running a command (git, ls, etc.), use the bash tool — never guess.
75
+ - Always use your tools. If a question can be answered by running a command (git, ls, etc.), use the bash tool. Never guess.
70
76
  - Always read a file before editing it.
71
77
  - If edit_file fails with "old_string not found", re-read the file to get the actual current content before retrying. Never guess at file contents.
72
78
  - Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
@@ -692,7 +698,7 @@ def run_prompt(client, prompt, system):
692
698
 
693
699
  plan_messages = [
694
700
  {"role": "system", "content": system},
695
- {"role": "user", "content": f"{prompt}\n\nBefore making any code changes, please analyze this issue and explain:\n1. What is the problem/goal?\n2. What files do you need to examine first?\n3. What is your overall approach to solving this?\n4. Which files will you modify and how?\n\nDo NOT write any code yet - just explain your plan."}
701
+ {"role": "user", "content": f"{prompt}\n\nBefore making any code changes, briefly state:\n1. The goal\n2. Files to examine\n3. Files to modify and how\n\nKeep it short. Do NOT write any code yet."}
696
702
  ]
697
703
 
698
704
  try:
@@ -924,7 +930,11 @@ def main():
924
930
  sys.exit(1)
925
931
 
926
932
  base_url = os.environ.get("CHORUS_API_URL", "https://chorus-bad0f.web.app/v1")
927
- client = OpenAI(api_key=api_key, base_url=base_url)
933
+ machine_id = os.environ.get("CHORUS_MACHINE_ID")
934
+ client_kwargs = {"api_key": api_key, "base_url": base_url}
935
+ if machine_id:
936
+ client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
937
+ client = OpenAI(**client_kwargs)
928
938
  system = SYSTEM_PROMPT.format(cwd=os.getcwd(), approach=APPROACH_BLOCK)
929
939
 
930
940
  # Load codebase map if available
package/tools/qa.py CHANGED
@@ -403,7 +403,11 @@ Write a clear numbered list of requirements. Each requirement should be specific
403
403
  def run_qa_chat(issue_context, messenger, qa_name):
404
404
  api_key = os.environ.get("CHORUS_API_KEY")
405
405
  base_url = os.environ.get("CHORUS_API_URL", "https://chorus-bad0f.web.app/v1")
406
- client = OpenAI(api_key=api_key, base_url=base_url)
406
+ machine_id = os.environ.get("CHORUS_MACHINE_ID")
407
+ client_kwargs = {"api_key": api_key, "base_url": base_url}
408
+ if machine_id:
409
+ client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
410
+ client = OpenAI(**client_kwargs)
407
411
  conversation = []
408
412
  raw_responses = []
409
413