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.
- package/README.md +11 -12
- package/dist/cli.js +161 -44
- 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
|
-
|
|
23
|
+
tmg init
|
|
27
24
|
|
|
28
25
|
# Add tasks from the CLI
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
+
**Important:** \`tmg start\` is a long-running daemon that needs its own terminal. Do NOT run it inline here.
|
|
84
84
|
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
406
|
+
child.kill();
|
|
387
407
|
}, timeoutMs);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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("
|
|
398
|
-
|
|
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", (
|
|
406
|
-
|
|
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
|
|
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", [
|
|
568
|
+
execFileSync2("which", [cli], { stdio: "ignore" });
|
|
569
|
+
return true;
|
|
552
570
|
} catch {
|
|
553
|
-
|
|
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
|
|
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 =
|
|
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}` :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
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
|
-
|
|
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("");
|