trismegistus 0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/dist/cli.js +569 -0
  4. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jessekaff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # trismegistus
2
+
3
+ A local persistent daemon that runs Claude Code sessions from a task queue. Add tasks to a markdown file, start the daemon, and walk away — it works through your list overnight, retries failures with context, and lets you steer it from your phone.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -D trismegistus
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g trismegistus
15
+ ```
16
+
17
+ ### Prerequisites
18
+
19
+ - Node.js >= 18
20
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`npm install -g @anthropic-ai/claude-code`)
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ # Initialize in your project
26
+ npx tmg init
27
+
28
+ # Add tasks from the CLI
29
+ npx tmg add "Migrate user model to TypeScript"
30
+ npx tmg add "Write tests for the payment module"
31
+
32
+ # Start the daemon
33
+ npx tmg start
34
+ ```
35
+
36
+ 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.
37
+
38
+ ## Commands
39
+
40
+ | Command | Description |
41
+ |---------|-------------|
42
+ | `tmg init` | Create `.trismegistus/` folder with config, tasks, and notes files |
43
+ | `tmg add "task"` | Add a task to the queue |
44
+ | `tmg start` | Start the daemon — runs tasks continuously until the queue is empty |
45
+ | `tmg status` | Show counts of pending, in-progress, done, retrying, and gave-up tasks |
46
+ | `tmg remote` | Open a VS Code tunnel with QR code for phone access |
47
+ | `tmg reset` | Reset all gave-up `[!!!]` tasks back to pending `[ ]` |
48
+ | `tmg -v` | Show version |
49
+
50
+ ## Task File Format
51
+
52
+ Tasks live in `.trismegistus/tasks.md` — plain markdown with checkbox syntax:
53
+
54
+ ```markdown
55
+ - [ ] Add authentication to the API
56
+ - [ ] Write tests for the payment module
57
+ - [ ] Refactor database queries to use connection pooling
58
+ ```
59
+
60
+ The daemon updates statuses as it works:
61
+
62
+ | Marker | Meaning |
63
+ |--------|---------|
64
+ | `[ ]` | Pending — waiting to run |
65
+ | `[~]` | In progress — currently running |
66
+ | `[x]` | Done |
67
+ | `[!]` | Failed once — will retry |
68
+ | `[!!]` | Failed twice — will retry |
69
+ | `[!!!]` | Gave up — exceeded max retries |
70
+
71
+ ## Workflows & Use Cases
72
+
73
+ ### Run tasks overnight
74
+
75
+ The core use case. Queue up a list of tasks before bed, start the daemon, and wake up to completed work.
76
+
77
+ ```bash
78
+ # Add your tasks
79
+ cat >> .trismegistus/tasks.md << 'EOF'
80
+ - [ ] Migrate user model to TypeScript
81
+ - [ ] Add input validation to all API routes
82
+ - [ ] Write integration tests for the checkout flow
83
+ EOF
84
+
85
+ # Start and let it run
86
+ tmg start
87
+ ```
88
+
89
+ Each task gets a full Claude Code session with autonomous permissions — it can read files, write code, run commands, and commit.
90
+
91
+ ### Leave notes for Claude
92
+
93
+ While the daemon is running, drop notes in `.trismegistus/notes.md`. The daemon reads and clears them before starting the next task — so you can steer it locally or from any device that can edit the file.
94
+
95
+ ```bash
96
+ echo "Use Prisma instead of raw SQL for the database tasks" >> .trismegistus/notes.md
97
+ ```
98
+
99
+ Notes are passed to Claude with priority, so you can redirect approach, add context, or give instructions without stopping the daemon. You can even ask Claude to add new tasks — it will edit `tasks.md` itself, and the daemon picks them up next cycle.
100
+
101
+ ### Add tasks while it's running
102
+
103
+ The task file is just markdown. Add new lines while the daemon is running and it will pick them up when the current task finishes.
104
+
105
+ ```bash
106
+ echo "- [ ] Fix the bug in the login form" >> .trismegistus/tasks.md
107
+ ```
108
+
109
+ ### Monitor from your phone
110
+
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.
112
+
113
+ ```bash
114
+ tmg remote
115
+ ```
116
+
117
+ Prerequisites: VS Code `code` CLI installed, GitHub account (one-time device auth on first use).
118
+
119
+ You can set a custom tunnel name:
120
+
121
+ ```bash
122
+ tmg remote --name my-machine
123
+ ```
124
+
125
+ ### Check progress remotely
126
+
127
+ ```bash
128
+ tmg status
129
+ ```
130
+
131
+ ```
132
+ TMG Status
133
+ ─────────────
134
+ Pending: 3
135
+ In Progress: 1
136
+ Done: 5
137
+ Retrying (!): 0
138
+ Retrying (!!): 0
139
+ Gave up (!!!): 1
140
+ ```
141
+
142
+ ### Retry failed tasks
143
+
144
+ When a task fails 3 times, it's marked `[!!!]` and skipped. After fixing the underlying issue, reset all gave-up tasks:
145
+
146
+ ```bash
147
+ tmg reset
148
+ tmg start
149
+ ```
150
+
151
+ ### Automatic handoff between retries
152
+
153
+ When a task fails, Claude writes a handoff summary to `.trismegistus/handoff`. The next attempt receives this context so it can pick up where the previous session left off rather than starting from scratch.
154
+
155
+ ## Configuration
156
+
157
+ Edit `.trismegistus/config` to tune the daemon:
158
+
159
+ ```
160
+ MAX_RETRIES=3 # Attempts per task before giving up
161
+ TIMEOUT_MINUTES=30 # Max runtime per task
162
+ IDLE_POLL_SECONDS=10 # Poll interval when no tasks available
163
+ TASK_DELAY_SECONDS=5 # Pause between tasks
164
+ ```
165
+
166
+ ## How It Works
167
+
168
+ 1. `tmg start` runs preflight checks (directory exists, Claude CLI installed)
169
+ 2. The daemon polls `tasks.md` for the next pending or retryable task
170
+ 3. It reads any human notes from `notes.md` (then clears the file)
171
+ 4. It reads any handoff context from a previous failed attempt
172
+ 5. It spawns `claude --dangerously-skip-permissions` with a constructed prompt
173
+ 6. On success: marks the task `[x]` and moves on
174
+ 7. On failure: escalates the status (`[ ]` -> `[!]` -> `[!!]` -> `[!!!]`), saves handoff context, and retries
175
+ 8. When the queue is empty, it idles and watches for new tasks
176
+
177
+ ## Project Structure
178
+
179
+ ```
180
+ .trismegistus/
181
+ config # Daemon configuration
182
+ tasks.md # Your task queue
183
+ notes.md # Notes for Claude (cleared after each read)
184
+ handoff # Context passed between retry attempts (auto-managed)
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import { createRequire as createRequire2 } from "module";
6
+ import { hostname } from "os";
7
+ import qrcode from "qrcode-terminal";
8
+
9
+ // src/init.ts
10
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ // src/types.ts
14
+ var DEFAULT_CONFIG = {
15
+ maxRetries: 3,
16
+ timeoutMinutes: 30,
17
+ idlePollSeconds: 10,
18
+ taskDelaySeconds: 5
19
+ };
20
+ var DIR_NAME = ".trismegistus";
21
+ var CONFIG_FILE = "config";
22
+ var TASKS_FILE = "tasks.md";
23
+ var NOTES_FILE = "notes.md";
24
+ var HANDOFF_FILE = "handoff";
25
+ var TASKS_TEMPLATE = `# Tasks \u2014 one per line
26
+ # - [ ] = pending - [x] = done
27
+ # - [!] = failed once (retrying) - [!!] = twice - [!!!] = gave up
28
+
29
+ - [ ] Example: replace with your first real task
30
+ `;
31
+ var NOTES_TEMPLATE = `# Notes for Claude \u2014 write here, cleared after each read
32
+ `;
33
+ var CONFIG_TEMPLATE = `# Trismegistus Configuration
34
+ MAX_RETRIES=3
35
+ TIMEOUT_MINUTES=30
36
+ IDLE_POLL_SECONDS=10
37
+ TASK_DELAY_SECONDS=5
38
+ `;
39
+ var STATUS_PRIORITY = [" ", "!", "!!"];
40
+
41
+ // src/init.ts
42
+ function initProject(projectDir) {
43
+ const tmgDir = join(projectDir, DIR_NAME);
44
+ const result = { created: [], skipped: [] };
45
+ if (!existsSync(tmgDir)) {
46
+ mkdirSync(tmgDir, { recursive: true });
47
+ }
48
+ const files = [
49
+ { name: CONFIG_FILE, content: CONFIG_TEMPLATE },
50
+ { name: TASKS_FILE, content: TASKS_TEMPLATE },
51
+ { name: NOTES_FILE, content: NOTES_TEMPLATE }
52
+ ];
53
+ for (const file of files) {
54
+ const path = join(tmgDir, file.name);
55
+ if (existsSync(path)) {
56
+ result.skipped.push(file.name);
57
+ } else {
58
+ writeFileSync(path, file.content);
59
+ result.created.push(file.name);
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ // src/tasks.ts
66
+ import { readFileSync, writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync } from "fs";
67
+ import { join as join2 } from "path";
68
+ var TASK_REGEX = /^- \[([^\]]*)\] (.+)$/;
69
+ function tasksPath(projectDir) {
70
+ return join2(projectDir, DIR_NAME, TASKS_FILE);
71
+ }
72
+ function notesPath(projectDir) {
73
+ return join2(projectDir, DIR_NAME, NOTES_FILE);
74
+ }
75
+ function handoffPath(projectDir) {
76
+ return join2(projectDir, DIR_NAME, HANDOFF_FILE);
77
+ }
78
+ function parseTasks(content) {
79
+ const tasks = [];
80
+ const lines = content.split("\n");
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const match = lines[i].match(TASK_REGEX);
83
+ if (match) {
84
+ const status = match[1];
85
+ tasks.push({ status, text: match[2], line: i + 1 });
86
+ }
87
+ }
88
+ return tasks;
89
+ }
90
+ function readTasks(projectDir) {
91
+ const path = tasksPath(projectDir);
92
+ if (!existsSync2(path)) return [];
93
+ return parseTasks(readFileSync(path, "utf-8"));
94
+ }
95
+ function getNextTask(projectDir) {
96
+ const tasks = readTasks(projectDir);
97
+ for (const status of STATUS_PRIORITY) {
98
+ const task = tasks.find((t) => t.status === status);
99
+ if (task) return task;
100
+ }
101
+ return null;
102
+ }
103
+ function setTaskStatus(projectDir, taskText, newStatus) {
104
+ const path = tasksPath(projectDir);
105
+ const content = readFileSync(path, "utf-8");
106
+ const lines = content.split("\n");
107
+ let found = false;
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const match = lines[i].match(TASK_REGEX);
110
+ if (match && match[2] === taskText) {
111
+ lines[i] = `- [${newStatus}] ${taskText}`;
112
+ found = true;
113
+ break;
114
+ }
115
+ }
116
+ if (found) {
117
+ writeFileSync2(path, lines.join("\n"));
118
+ }
119
+ return found;
120
+ }
121
+ function getTaskCounts(projectDir) {
122
+ const tasks = readTasks(projectDir);
123
+ return {
124
+ pending: tasks.filter((t) => t.status === " ").length,
125
+ inProgress: tasks.filter((t) => t.status === "~").length,
126
+ done: tasks.filter((t) => t.status === "x").length,
127
+ failed1: tasks.filter((t) => t.status === "!").length,
128
+ failed2: tasks.filter((t) => t.status === "!!").length,
129
+ gaveUp: tasks.filter((t) => t.status === "!!!").length
130
+ };
131
+ }
132
+ function resetGaveUpTasks(projectDir) {
133
+ const path = tasksPath(projectDir);
134
+ if (!existsSync2(path)) return 0;
135
+ const content = readFileSync(path, "utf-8");
136
+ const lines = content.split("\n");
137
+ let count = 0;
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const match = lines[i].match(TASK_REGEX);
140
+ if (match && match[1] === "!!!") {
141
+ lines[i] = `- [ ] ${match[2]}`;
142
+ count++;
143
+ }
144
+ }
145
+ if (count > 0) writeFileSync2(path, lines.join("\n"));
146
+ return count;
147
+ }
148
+ function readAndClearNotes(projectDir) {
149
+ const path = notesPath(projectDir);
150
+ if (!existsSync2(path)) return "";
151
+ const content = readFileSync(path, "utf-8");
152
+ const notes = content.split("\n").filter((l) => !l.startsWith("#") && l.trim() !== "").slice(0, 50).join("\n");
153
+ writeFileSync2(
154
+ path,
155
+ "# Notes for Claude \u2014 write here, cleared after each read\n"
156
+ );
157
+ return notes;
158
+ }
159
+ function readHandoff(projectDir) {
160
+ const path = handoffPath(projectDir);
161
+ if (!existsSync2(path)) return "";
162
+ return readFileSync(path, "utf-8");
163
+ }
164
+ function writeHandoff(projectDir, content) {
165
+ writeFileSync2(handoffPath(projectDir), content);
166
+ }
167
+ function deleteHandoff(projectDir) {
168
+ const path = handoffPath(projectDir);
169
+ if (existsSync2(path)) {
170
+ unlinkSync(path);
171
+ }
172
+ }
173
+ function getAttemptFromStatus(status) {
174
+ switch (status) {
175
+ case "!":
176
+ return 2;
177
+ case "!!":
178
+ return 3;
179
+ default:
180
+ return 1;
181
+ }
182
+ }
183
+ function addTask(projectDir, text) {
184
+ const trimmed = text.trim();
185
+ if (!trimmed) {
186
+ throw new Error("Task text cannot be empty.");
187
+ }
188
+ const path = tasksPath(projectDir);
189
+ if (!existsSync2(path)) {
190
+ throw new Error(
191
+ "No tasks file found. Run `tmg init` first."
192
+ );
193
+ }
194
+ const content = readFileSync(path, "utf-8");
195
+ const line = `- [ ] ${trimmed}
196
+ `;
197
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
198
+ writeFileSync2(path, content + separator + line);
199
+ }
200
+ function getFailureStatus(attempt) {
201
+ if (attempt === 1) return "!";
202
+ if (attempt === 2) return "!!";
203
+ return "!!!";
204
+ }
205
+
206
+ // src/daemon.ts
207
+ import { existsSync as existsSync4 } from "fs";
208
+ import { join as join4 } from "path";
209
+ import { execFileSync } from "child_process";
210
+
211
+ // src/config.ts
212
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
213
+ import { join as join3 } from "path";
214
+ var KEY_MAP = {
215
+ MAX_RETRIES: "maxRetries",
216
+ TIMEOUT_MINUTES: "timeoutMinutes",
217
+ IDLE_POLL_SECONDS: "idlePollSeconds",
218
+ TASK_DELAY_SECONDS: "taskDelaySeconds"
219
+ };
220
+ function parseConfigFile(content) {
221
+ const result = {};
222
+ for (const line of content.split("\n")) {
223
+ const trimmed = line.trim();
224
+ if (!trimmed || trimmed.startsWith("#")) continue;
225
+ const eqIndex = trimmed.indexOf("=");
226
+ if (eqIndex === -1) continue;
227
+ const key = trimmed.slice(0, eqIndex).trim();
228
+ const value = trimmed.slice(eqIndex + 1).trim();
229
+ const configKey = KEY_MAP[key];
230
+ if (!configKey) continue;
231
+ const num = parseInt(value, 10);
232
+ if (!isNaN(num) && num >= 0) {
233
+ result[configKey] = num;
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+ function loadConfig(projectDir) {
239
+ const configPath = join3(projectDir, DIR_NAME, CONFIG_FILE);
240
+ if (!existsSync3(configPath)) {
241
+ return { ...DEFAULT_CONFIG };
242
+ }
243
+ const content = readFileSync2(configPath, "utf-8");
244
+ const overrides = parseConfigFile(content);
245
+ return { ...DEFAULT_CONFIG, ...overrides };
246
+ }
247
+
248
+ // src/runner.ts
249
+ import { spawn } from "child_process";
250
+ function runClaude(prompt, timeoutMs, projectDir, spawnFn = spawn) {
251
+ return new Promise((resolve) => {
252
+ const ac = new AbortController();
253
+ let timedOut = false;
254
+ const timer = setTimeout(() => {
255
+ timedOut = true;
256
+ ac.abort();
257
+ }, timeoutMs);
258
+ const child = spawnFn(
259
+ "claude",
260
+ ["--dangerously-skip-permissions", "-p", prompt],
261
+ {
262
+ cwd: projectDir,
263
+ stdio: "inherit",
264
+ signal: ac.signal
265
+ }
266
+ );
267
+ child.on("close", (code) => {
268
+ clearTimeout(timer);
269
+ resolve({
270
+ success: !timedOut && code === 0,
271
+ exitCode: timedOut ? 124 : code ?? 1,
272
+ timedOut
273
+ });
274
+ });
275
+ child.on("error", (err) => {
276
+ clearTimeout(timer);
277
+ resolve({
278
+ success: false,
279
+ exitCode: 1,
280
+ timedOut: timedOut || err.name === "AbortError"
281
+ });
282
+ });
283
+ });
284
+ }
285
+
286
+ // src/daemon.ts
287
+ import { createRequire } from "module";
288
+ var require2 = createRequire(import.meta.url);
289
+ var VERSION = require2("../package.json").version;
290
+ function preflight(projectDir) {
291
+ const errors = [];
292
+ const warnings = [];
293
+ const tmgDir = join4(projectDir, DIR_NAME);
294
+ if (!existsSync4(tmgDir)) {
295
+ errors.push(`.trismegistus/ not found. Run: tmg init`);
296
+ return { ok: false, errors, warnings };
297
+ }
298
+ if (!existsSync4(join4(tmgDir, TASKS_FILE))) {
299
+ errors.push(`tasks.md not found in .trismegistus/. Run: tmg init`);
300
+ return { ok: false, errors, warnings };
301
+ }
302
+ try {
303
+ execFileSync("which", ["claude"], { stdio: "ignore" });
304
+ } catch {
305
+ errors.push(
306
+ "Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code"
307
+ );
308
+ return { ok: false, errors, warnings };
309
+ }
310
+ try {
311
+ execFileSync("claude", ["--version"], { stdio: "ignore" });
312
+ } catch {
313
+ warnings.push(
314
+ "Claude CLI found but 'claude --version' failed. Authentication may be needed."
315
+ );
316
+ }
317
+ return { ok: errors.length === 0, errors, warnings };
318
+ }
319
+ function buildPrompt(taskText, attempt, maxRetries, notes, handoff) {
320
+ let prompt = `You are running autonomously overnight (attempt ${attempt}/${maxRetries}). Complete this task fully, commit your work, don't ask questions.
321
+
322
+ If you are running low on time, write a summary of what you did and what remains to .trismegistus/handoff so the next session can continue.`;
323
+ if (notes) {
324
+ prompt += `
325
+
326
+ NOTES FROM HUMAN (priority):
327
+ ${notes}`;
328
+ }
329
+ if (handoff) {
330
+ prompt += `
331
+
332
+ CONTEXT FROM PREVIOUS ATTEMPT (pick up where this left off):
333
+ ${handoff}`;
334
+ }
335
+ prompt += `
336
+
337
+ Task: ${taskText}`;
338
+ return prompt;
339
+ }
340
+ function sleep(ms) {
341
+ return new Promise((resolve) => setTimeout(resolve, ms));
342
+ }
343
+ async function runDaemon(opts) {
344
+ const { projectDir, spawnFn, maxIterations, onLog } = opts;
345
+ const log = onLog ?? ((msg) => console.log(msg));
346
+ const config = loadConfig(projectDir);
347
+ log(` TMG v${VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
348
+ log(` Project: ${projectDir}`);
349
+ log(` Timeout: ${config.timeoutMinutes}m | Max retries: ${config.maxRetries}`);
350
+ log("");
351
+ let iterations = 0;
352
+ while (true) {
353
+ if (maxIterations !== void 0 && iterations >= maxIterations) break;
354
+ iterations++;
355
+ const task = getNextTask(projectDir);
356
+ if (!task) {
357
+ log(`[${time()}] No tasks. Watching...`);
358
+ await sleep(config.idlePollSeconds * 1e3);
359
+ continue;
360
+ }
361
+ const attempt = getAttemptFromStatus(task.status);
362
+ const notes = readAndClearNotes(projectDir);
363
+ const handoff = readHandoff(projectDir);
364
+ log("");
365
+ log("\u2501".repeat(47));
366
+ log(`\u25B6 [${time()}] ${task.text} (attempt ${attempt}/${config.maxRetries})`);
367
+ log("\u2501".repeat(47));
368
+ setTaskStatus(projectDir, task.text, "~");
369
+ const prompt = buildPrompt(
370
+ task.text,
371
+ attempt,
372
+ config.maxRetries,
373
+ notes,
374
+ handoff
375
+ );
376
+ const result = await runClaude(
377
+ prompt,
378
+ config.timeoutMinutes * 60 * 1e3,
379
+ projectDir,
380
+ spawnFn
381
+ );
382
+ if (result.success) {
383
+ setTaskStatus(projectDir, task.text, "x");
384
+ deleteHandoff(projectDir);
385
+ log(`[${time()}] Done: ${task.text}`);
386
+ } else {
387
+ if (result.timedOut) {
388
+ log(`[${time()}] Timed out`);
389
+ } else {
390
+ log(`[${time()}] Exited (${result.exitCode})`);
391
+ }
392
+ if (!existsSync4(join4(projectDir, DIR_NAME, "handoff"))) {
393
+ const reason = result.timedOut ? "timed out" : "failed";
394
+ writeHandoff(
395
+ projectDir,
396
+ `Previous attempt ${reason}. Check git log and codebase to see what was done. Continue from there.`
397
+ );
398
+ }
399
+ if (attempt >= config.maxRetries) {
400
+ setTaskStatus(projectDir, task.text, "!!!");
401
+ deleteHandoff(projectDir);
402
+ log(`Gave up after ${config.maxRetries} attempts: ${task.text}`);
403
+ } else {
404
+ const failStatus = getFailureStatus(attempt);
405
+ setTaskStatus(projectDir, task.text, failStatus);
406
+ log(` \u2192 Will retry with handoff context...`);
407
+ }
408
+ }
409
+ await sleep(config.taskDelaySeconds * 1e3);
410
+ }
411
+ }
412
+ function time() {
413
+ const now = /* @__PURE__ */ new Date();
414
+ return `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}`;
415
+ }
416
+
417
+ // src/tunnel.ts
418
+ import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
419
+ function checkCodeCli() {
420
+ try {
421
+ execFileSync2("which", ["code"], { stdio: "ignore" });
422
+ } catch {
423
+ throw new Error(
424
+ "VS Code CLI (code) not found. Install VS Code or download the standalone CLI: https://code.visualstudio.com/docs/editor/command-line"
425
+ );
426
+ }
427
+ }
428
+ function startTunnel(name) {
429
+ return new Promise((resolve, reject) => {
430
+ const child = spawn2(
431
+ "code",
432
+ ["tunnel", "--name", name, "--accept-server-license-terms"],
433
+ { stdio: ["ignore", "pipe", "pipe"] }
434
+ );
435
+ let stderr = "";
436
+ let settled = false;
437
+ const TIMEOUT_MS = 6e4;
438
+ function settle(fn) {
439
+ if (!settled) {
440
+ settled = true;
441
+ fn();
442
+ }
443
+ }
444
+ const timer = setTimeout(() => {
445
+ child.kill();
446
+ const msg = stderr ? `Timed out waiting for tunnel URL. stderr:
447
+ ${stderr}` : "Timed out waiting for tunnel URL. You may need to authenticate \u2014 run `code tunnel` manually first.";
448
+ settle(() => reject(new Error(msg)));
449
+ }, TIMEOUT_MS);
450
+ child.stderr?.on("data", (chunk) => {
451
+ stderr += chunk.toString();
452
+ });
453
+ child.stdout?.on("data", (chunk) => {
454
+ const text = chunk.toString();
455
+ const match = text.match(/(https:\/\/vscode\.dev\/tunnel\/[^\s]+)/);
456
+ if (match) {
457
+ clearTimeout(timer);
458
+ settle(() => resolve({ url: match[1], process: child }));
459
+ }
460
+ });
461
+ child.on("error", (err) => {
462
+ clearTimeout(timer);
463
+ settle(() => reject(err));
464
+ });
465
+ child.on("close", (code) => {
466
+ clearTimeout(timer);
467
+ settle(() => reject(new Error(`code tunnel exited with code ${code}. stderr:
468
+ ${stderr}`)));
469
+ });
470
+ });
471
+ }
472
+
473
+ // src/cli.ts
474
+ var program = new Command();
475
+ program.name("tmg").description("Trismegistus \u2014 Task Manager for Claude Code").version(
476
+ (() => {
477
+ const req = createRequire2(import.meta.url);
478
+ return req("../package.json").version;
479
+ })(),
480
+ "-v, --version"
481
+ );
482
+ program.command("init").description("Create .trismegistus/ folder in current directory").action(() => {
483
+ const result = initProject(process.cwd());
484
+ for (const name of result.created) {
485
+ console.log(` Created ${name}`);
486
+ }
487
+ for (const name of result.skipped) {
488
+ console.log(` Skipped ${name} (already exists)`);
489
+ }
490
+ console.log("");
491
+ console.log(" Add your tasks to .trismegistus/tasks.md, then run: tmg start");
492
+ console.log("");
493
+ });
494
+ program.command("status").description("Show task counts").action(() => {
495
+ const counts = getTaskCounts(process.cwd());
496
+ console.log("");
497
+ console.log(" TMG Status");
498
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
499
+ console.log(` Pending: ${counts.pending}`);
500
+ console.log(` In Progress: ${counts.inProgress}`);
501
+ console.log(` Done: ${counts.done}`);
502
+ console.log(` Retrying (!): ${counts.failed1}`);
503
+ console.log(` Retrying (!!): ${counts.failed2}`);
504
+ console.log(` Gave up (!!!): ${counts.gaveUp}`);
505
+ console.log("");
506
+ });
507
+ program.command("start").description("Start the daemon \u2014 continuously runs tasks from the queue").action(async () => {
508
+ const check = preflight(process.cwd());
509
+ for (const err of check.errors) {
510
+ console.error(` Error: ${err}`);
511
+ }
512
+ for (const warn of check.warnings) {
513
+ console.warn(` Warning: ${warn}`);
514
+ }
515
+ if (!check.ok) {
516
+ process.exit(1);
517
+ }
518
+ console.log("");
519
+ await runDaemon({ projectDir: process.cwd() });
520
+ });
521
+ program.command("add").description("Add a task to the queue").argument("<text>", "Task description").action((text) => {
522
+ try {
523
+ addTask(process.cwd(), text);
524
+ console.log(`Added: ${text}`);
525
+ } catch (e) {
526
+ console.error(e instanceof Error ? e.message : String(e));
527
+ process.exit(1);
528
+ }
529
+ });
530
+ program.command("reset").description("Reset all gave-up [!!!] tasks back to pending [ ]").action(() => {
531
+ const count = resetGaveUpTasks(process.cwd());
532
+ if (count === 0) {
533
+ console.log(" No gave-up tasks to reset.");
534
+ } else {
535
+ console.log(` Reset ${count} task(s) back to pending.`);
536
+ }
537
+ });
538
+ program.command("remote").description("Open a VS Code tunnel for phone access (QR code)").option("--name <name>", "Tunnel name").action(async (opts) => {
539
+ const tunnelName = opts.name ?? hostname();
540
+ try {
541
+ checkCodeCli();
542
+ } catch (e) {
543
+ console.error(e instanceof Error ? e.message : String(e));
544
+ process.exit(1);
545
+ }
546
+ console.log("Starting VS Code tunnel...");
547
+ try {
548
+ const { url, process: tunnelProc } = await startTunnel(tunnelName);
549
+ console.log("");
550
+ console.log(` URL: ${url}`);
551
+ console.log("");
552
+ qrcode.generate(url, { small: true }, (code) => {
553
+ console.log(code);
554
+ });
555
+ console.log(" Scan the QR code or open the URL on your phone");
556
+ console.log(" Press Ctrl+C to stop the tunnel");
557
+ console.log("");
558
+ const cleanup = () => {
559
+ tunnelProc.kill();
560
+ process.exit(0);
561
+ };
562
+ process.on("SIGINT", cleanup);
563
+ process.on("SIGTERM", cleanup);
564
+ } catch (e) {
565
+ console.error(e instanceof Error ? e.message : String(e));
566
+ process.exit(1);
567
+ }
568
+ });
569
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "trismegistus",
3
+ "version": "0.1.0",
4
+ "description": "A local persistent daemon that runs AI sessions from a task queue, with mobile support.",
5
+ "type": "module",
6
+ "main": "./dist/cli.js",
7
+ "bin": {
8
+ "tmg": "./dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsx src/cli.ts",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "claude-code",
20
+ "task-manager",
21
+ "daemon",
22
+ "automation",
23
+ "ai"
24
+ ],
25
+ "author": "jessekaff",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/jessekaff/trismegistus.git"
30
+ },
31
+ "homepage": "https://github.com/jessekaff/trismegistus#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/jessekaff/trismegistus/issues"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "dependencies": {
42
+ "commander": "^13.0.0",
43
+ "qrcode-terminal": "^0.12.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "@types/qrcode-terminal": "^0.12.2",
48
+ "tsup": "^8.0.0",
49
+ "tsx": "^4.0.0",
50
+ "typescript": "^5.7.0",
51
+ "vitest": "^3.0.0"
52
+ }
53
+ }