trismegistus 1.0.2 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +11 -12
  2. package/dist/cli.js +161 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,33 +4,30 @@ A local persistent daemon that runs Claude Code sessions from a task queue. Add
4
4
 
5
5
  ## Install
6
6
 
7
- ```bash
8
- npm install -D trismegistus
9
- ```
10
-
11
- Or install globally:
12
-
13
7
  ```bash
14
8
  npm install -g trismegistus
15
9
  ```
16
10
 
11
+ This gives you the `tmg` command globally — use it from any project.
12
+
17
13
  ### Prerequisites
18
14
 
19
15
  - Node.js >= 18
20
16
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`npm install -g @anthropic-ai/claude-code`)
17
+ - Enable remote control in your Claude Code settings to monitor sessions from claude.ai/code or the Claude mobile app (`remote_control: true` for all sessions)
21
18
 
22
19
  ## Quick Start
23
20
 
24
21
  ```bash
25
22
  # Initialize in your project
26
- npx tmg init
23
+ tmg init
27
24
 
28
25
  # Add tasks from the CLI
29
- npx tmg add "Migrate user model to TypeScript"
30
- npx tmg add "Write tests for the payment module"
26
+ tmg add "Migrate user model to TypeScript"
27
+ tmg add "Write tests for the payment module"
31
28
 
32
29
  # Start the daemon
33
- npx tmg start
30
+ tmg start
34
31
  ```
35
32
 
36
33
  The daemon picks up tasks one at a time, runs each in a full Claude Code session with `--dangerously-skip-permissions`, commits the work, and moves on to the next.
@@ -108,13 +105,15 @@ echo "- [ ] Fix the bug in the login form" >> .trismegistus/tasks.md
108
105
 
109
106
  ### Monitor from your phone
110
107
 
111
- `tmg remote` creates a secure VS Code tunnel through Microsoft's Azure relay and prints a QR code. Scan it on your phone to get a full VS Code UI — including terminal access — right in the browser. No port forwarding or same-network requirement.
108
+ `tmg remote` creates a secure tunnel through Microsoft's Azure relay and prints a QR code. Scan it on your phone to get a full editor UI — including terminal access — right in the browser. No port forwarding or same-network requirement.
109
+
110
+ Works with both VS Code and Cursor — it auto-detects which editor you're using and runs the correct tunnel command.
112
111
 
113
112
  ```bash
114
113
  tmg remote
115
114
  ```
116
115
 
117
- Prerequisites: VS Code `code` CLI installed, GitHub account (one-time device auth on first use).
116
+ Prerequisites: VS Code (`code`) or Cursor (`cursor`) CLI installed, GitHub account (one-time device auth on first use).
118
117
 
119
118
  You can set a custom tunnel name:
120
119
 
package/dist/cli.js CHANGED
@@ -80,9 +80,20 @@ var CLAUDE_COMMANDS = [
80
80
  name: "tmg.md",
81
81
  content: `Start the Trismegistus daemon to continuously run tasks from the queue.
82
82
 
83
- Run: \`tmg start\`
83
+ **Important:** \`tmg start\` is a long-running daemon that needs its own terminal. Do NOT run it inline here.
84
84
 
85
- The daemon picks up pending tasks from \`.trismegistus/tasks.md\` and executes them one by one, idling and watching for new tasks when the queue is empty.
85
+ Run this command in a **separate terminal**:
86
+
87
+ \`\`\`
88
+ tmg start
89
+ \`\`\`
90
+
91
+ The daemon:
92
+ - Picks up pending tasks from \`.trismegistus/tasks.md\`
93
+ - Opens a VS Code tunnel for remote editor access (QR code)
94
+ - Idles and watches for new tasks when the queue is empty
95
+
96
+ **Note:** To monitor sessions remotely, enable remote control in your Claude Code settings (\`remote_control: true\` for all sessions).
86
97
 
87
98
  $ARGUMENTS
88
99
  `
@@ -377,38 +388,39 @@ function loadConfig(projectDir) {
377
388
 
378
389
  // src/runner.ts
379
390
  import { spawn } from "child_process";
380
- function runClaude(prompt, timeoutMs, projectDir, spawnFn = spawn) {
391
+ function defaultSpawn(command, args, cwd) {
392
+ return spawn(command, args, { cwd, stdio: "ignore" });
393
+ }
394
+ function runClaude(opts) {
395
+ const { prompt, timeoutMs, projectDir, spawnFn = defaultSpawn } = opts;
381
396
  return new Promise((resolve) => {
382
- const ac = new AbortController();
383
397
  let timedOut = false;
398
+ let settled = false;
399
+ const child = spawnFn(
400
+ "claude",
401
+ ["-p", prompt, "--dangerously-skip-permissions"],
402
+ projectDir
403
+ );
384
404
  const timer = setTimeout(() => {
385
405
  timedOut = true;
386
- ac.abort();
406
+ child.kill();
387
407
  }, timeoutMs);
388
- const child = spawnFn(
389
- "claude",
390
- ["--dangerously-skip-permissions", "-p", prompt],
391
- {
392
- cwd: projectDir,
393
- stdio: "inherit",
394
- signal: ac.signal
408
+ function settle(result) {
409
+ if (!settled) {
410
+ settled = true;
411
+ clearTimeout(timer);
412
+ resolve(result);
395
413
  }
396
- );
397
- child.on("close", (code) => {
398
- clearTimeout(timer);
399
- resolve({
414
+ }
415
+ child.on("exit", (code) => {
416
+ settle({
400
417
  success: !timedOut && code === 0,
401
418
  exitCode: timedOut ? 124 : code ?? 1,
402
419
  timedOut
403
420
  });
404
421
  });
405
- child.on("error", (err) => {
406
- clearTimeout(timer);
407
- resolve({
408
- success: false,
409
- exitCode: 1,
410
- timedOut: timedOut || err.name === "AbortError"
411
- });
422
+ child.on("error", () => {
423
+ settle({ success: false, exitCode: 1, timedOut: false });
412
424
  });
413
425
  });
414
426
  }
@@ -503,12 +515,12 @@ async function runDaemon(opts) {
503
515
  notes,
504
516
  handoff
505
517
  );
506
- const result = await runClaude(
518
+ const result = await runClaude({
507
519
  prompt,
508
- config.timeoutMinutes * 60 * 1e3,
520
+ timeoutMs: config.timeoutMinutes * 60 * 1e3,
509
521
  projectDir,
510
522
  spawnFn
511
- );
523
+ });
512
524
  if (result.success) {
513
525
  setTaskStatus(projectDir, task.text, "x");
514
526
  deleteHandoff(projectDir);
@@ -546,16 +558,33 @@ function time() {
546
558
 
547
559
  // src/tunnel.ts
548
560
  import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
549
- function checkCodeCli() {
561
+ function detectEditorCli() {
562
+ if (process.env.TERM_PROGRAM === "cursor") return "cursor";
563
+ if (process.env.CURSOR_TRACE_DIR) return "cursor";
564
+ return "code";
565
+ }
566
+ function cliExists(cli) {
550
567
  try {
551
- execFileSync2("which", ["code"], { stdio: "ignore" });
568
+ execFileSync2("which", [cli], { stdio: "ignore" });
569
+ return true;
552
570
  } catch {
553
- throw new Error(
554
- "VS Code CLI (code) not found. Install VS Code or download the standalone CLI: https://code.visualstudio.com/docs/editor/command-line"
555
- );
571
+ return false;
556
572
  }
557
573
  }
558
- function startTunnel(name) {
574
+ function isVSCodeAvailable() {
575
+ return cliExists("code");
576
+ }
577
+ function isCursorDetected() {
578
+ return detectEditorCli() === "cursor";
579
+ }
580
+ function startTunnel(name, options = {}) {
581
+ if (!isVSCodeAvailable()) {
582
+ return Promise.reject(
583
+ new Error(
584
+ '"code" CLI not found. Install VS Code or download the standalone CLI: https://code.visualstudio.com/docs/editor/command-line'
585
+ )
586
+ );
587
+ }
559
588
  return new Promise((resolve, reject) => {
560
589
  const child = spawn2(
561
590
  "code",
@@ -564,7 +593,7 @@ function startTunnel(name) {
564
593
  );
565
594
  let stderr = "";
566
595
  let settled = false;
567
- const TIMEOUT_MS = 6e4;
596
+ const TIMEOUT_MS = 12e4;
568
597
  function settle(fn) {
569
598
  if (!settled) {
570
599
  settled = true;
@@ -574,14 +603,25 @@ function startTunnel(name) {
574
603
  const timer = setTimeout(() => {
575
604
  child.kill();
576
605
  const msg = stderr ? `Timed out waiting for tunnel URL. stderr:
577
- ${stderr}` : "Timed out waiting for tunnel URL. You may need to authenticate \u2014 run `code tunnel` manually first.";
606
+ ${stderr}` : `Timed out waiting for tunnel URL. You may need to authenticate \u2014 run \`code tunnel\` manually first.`;
578
607
  settle(() => reject(new Error(msg)));
579
608
  }, TIMEOUT_MS);
580
609
  child.stderr?.on("data", (chunk) => {
581
- stderr += chunk.toString();
610
+ const text = chunk.toString();
611
+ stderr += text;
612
+ if (options.onOutput) {
613
+ for (const line of text.split("\n").filter(Boolean)) {
614
+ options.onOutput(line);
615
+ }
616
+ }
582
617
  });
583
618
  child.stdout?.on("data", (chunk) => {
584
619
  const text = chunk.toString();
620
+ if (options.onOutput) {
621
+ for (const line of text.split("\n").filter(Boolean)) {
622
+ options.onOutput(line);
623
+ }
624
+ }
585
625
  const match = text.match(/(https:\/\/vscode\.dev\/tunnel\/[^\s]+)/);
586
626
  if (match) {
587
627
  clearTimeout(timer);
@@ -634,7 +674,7 @@ program.command("status").description("Show task counts").action(() => {
634
674
  console.log(` Gave up (!!!): ${counts.gaveUp}`);
635
675
  console.log("");
636
676
  });
637
- program.command("start").description("Start the daemon \u2014 continuously runs tasks from the queue").action(async () => {
677
+ program.command("start").description("Start the daemon \u2014 continuously runs tasks from the queue").option("--no-tunnel", "Skip VS Code tunnel setup").action(async (opts) => {
638
678
  const check = preflight(process.cwd());
639
679
  for (const err of check.errors) {
640
680
  console.error(` Error: ${err}`);
@@ -646,7 +686,63 @@ program.command("start").description("Start the daemon \u2014 continuously runs
646
686
  process.exit(1);
647
687
  }
648
688
  console.log("");
649
- await runDaemon({ projectDir: process.cwd() });
689
+ let tunnelProc = null;
690
+ if (opts.tunnel) {
691
+ const editor = detectEditorCli();
692
+ if (editor === "cursor" || !isVSCodeAvailable()) {
693
+ if (isCursorDetected()) {
694
+ console.log(" Note: Remote tunnels only work with VS Code, not Cursor.");
695
+ } else {
696
+ console.log(" Note: VS Code CLI not found \u2014 skipping tunnel.");
697
+ }
698
+ console.log(" Tip: Enable remote control in your Claude Code settings to monitor sessions from claude.ai/code.");
699
+ console.log("");
700
+ } else {
701
+ const tunnelName = hostname();
702
+ console.log(" Starting VS Code tunnel...\n");
703
+ try {
704
+ const { url, process: tp } = await startTunnel(tunnelName, {
705
+ onOutput(line) {
706
+ const deviceCodeMatch = line.match(/use code\s+([A-Z0-9]{4}-[A-Z0-9]{4})/i);
707
+ const deviceUrlMatch = line.match(/(https:\/\/github\.com\/login\/device)/);
708
+ if (deviceCodeMatch || deviceUrlMatch) {
709
+ console.log("");
710
+ if (deviceUrlMatch) {
711
+ console.log(` Open: ${deviceUrlMatch[1]}`);
712
+ }
713
+ if (deviceCodeMatch) {
714
+ console.log(` Code: ${deviceCodeMatch[1]}`);
715
+ }
716
+ console.log("");
717
+ } else if (line.includes("Creating tunnel")) {
718
+ console.log(` ${line.replace(/^\[.*?\]\s*\w+\s*/, "")}`);
719
+ }
720
+ }
721
+ });
722
+ tunnelProc = tp;
723
+ console.log("");
724
+ console.log(` VS Code tunnel: ${url}`);
725
+ console.log("");
726
+ qrcode.generate(url, { small: true }, (code) => {
727
+ console.log(code);
728
+ });
729
+ console.log(" Scan QR to access editor remotely");
730
+ console.log("");
731
+ } catch (e) {
732
+ console.warn(` Warning: Tunnel failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
733
+ console.warn(" Continuing without tunnel.\n");
734
+ }
735
+ }
736
+ }
737
+ const cleanup = () => {
738
+ tunnelProc?.kill();
739
+ process.exit(0);
740
+ };
741
+ process.on("SIGINT", cleanup);
742
+ process.on("SIGTERM", cleanup);
743
+ await runDaemon({
744
+ projectDir: process.cwd()
745
+ });
650
746
  });
651
747
  program.command("add").description("Add a task to the queue").argument("<text>", "Task description").action((text) => {
652
748
  try {
@@ -675,16 +771,37 @@ program.command("reset").description("Reset all gave-up [!!!] tasks back to pend
675
771
  }
676
772
  });
677
773
  program.command("remote").description("Open a VS Code tunnel for phone access (QR code)").option("--name <name>", "Tunnel name").action(async (opts) => {
678
- const tunnelName = opts.name ?? hostname();
679
- try {
680
- checkCodeCli();
681
- } catch (e) {
682
- console.error(e instanceof Error ? e.message : String(e));
774
+ if (isCursorDetected()) {
775
+ console.warn(" Warning: Remote tunnels only work with VS Code, not Cursor.");
776
+ console.warn(" Attempting anyway with VS Code CLI...\n");
777
+ }
778
+ if (!isVSCodeAvailable()) {
779
+ console.error(
780
+ ' Error: "code" CLI not found. Install VS Code or download the standalone CLI:\n https://code.visualstudio.com/docs/editor/command-line'
781
+ );
683
782
  process.exit(1);
684
783
  }
685
- console.log("Starting VS Code tunnel...");
784
+ const tunnelName = opts.name ?? hostname();
785
+ console.log("Starting VS Code tunnel...\n");
686
786
  try {
687
- const { url, process: tunnelProc } = await startTunnel(tunnelName);
787
+ const { url, process: tunnelProc } = await startTunnel(tunnelName, {
788
+ onOutput(line) {
789
+ const deviceCodeMatch = line.match(/use code\s+([A-Z0-9]{4}-[A-Z0-9]{4})/i);
790
+ const deviceUrlMatch = line.match(/(https:\/\/github\.com\/login\/device)/);
791
+ if (deviceCodeMatch || deviceUrlMatch) {
792
+ console.log("");
793
+ if (deviceUrlMatch) {
794
+ console.log(` Open: ${deviceUrlMatch[1]}`);
795
+ }
796
+ if (deviceCodeMatch) {
797
+ console.log(` Code: ${deviceCodeMatch[1]}`);
798
+ }
799
+ console.log("");
800
+ } else if (line.includes("Creating tunnel")) {
801
+ console.log(` ${line.replace(/^\[.*?\]\s*\w+\s*/, "")}`);
802
+ }
803
+ }
804
+ });
688
805
  console.log("");
689
806
  console.log(` URL: ${url}`);
690
807
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trismegistus",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "A local persistent daemon that runs AI sessions from a task queue, with mobile support.",
5
5
  "type": "module",
6
6
  "bin": {