diff --git a/AGENTS.md b/AGENTS.md
index a533372..89bf1b4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
# threadle — agent instructions
threadle is a local-first node-graph patchbay for CLI agent sessions (Claude Code,
-opencode, Cursor Agent, Antigravity, Codex, GitHub Copilot, Grok Build): observability (blueprints, activity, statistics) plus context
+opencode, Cursor Agent, Antigravity, Codex, GitHub Copilot, Grok Build, Muse Code): observability (blueprints, activity, statistics) plus context
wiring/handoff between sessions on a canvas.
## Vocabulary (UI + docs)
@@ -34,7 +34,7 @@ Plain terms lead. Graph JSON keeps historical type ids for compatibility.
and the built-in `NodeDefinition` catalog (`src/nodes/`). Single source of
truth; UI and server both import it.
- `packages/server` — Hono API (`bin: threadle`). Providers under
- `src/providers/{claude-code,opencode,cursor,antigravity,codex,copilot,grok}` read agent storage **read-only**;
+ `src/providers/{claude-code,opencode,cursor,antigravity,codex,copilot,grok,muse}` read agent storage **read-only**;
all runs/injections go through each tool's CLI or HTTP surface.
- `packages/web` — Vue 3 + Vue Flow. True-black monochrome theme
(`src/theme/theme.css`); no emojis, text glyphs only (❯ ⎇ ⟨/⟩ ▤ ⚙ ✦).
@@ -49,7 +49,7 @@ Plain terms lead. Graph JSON keeps historical type ids for compatibility.
custom nodes / stdlib / recipes. Shared pure helpers live in
`packages/shared` (`judge`, `until`, `wait-idle`, `iterator`, …).
- Never write into `~/.claude/projects`, opencode's SQLite, `~/.cursor`
- chats/transcripts, `~/.gemini/antigravity-cli`, `~/.codex`, `~/.copilot`, or `~/.grok`; parsers must skip unknown
+ chats/transcripts, `~/.gemini/antigravity-cli`, `~/.codex`, `~/.copilot`, `~/.grok`, `~/.muse`, or `~/.local/share/muse`; parsers must skip unknown
record types, not throw (formats are undocumented and churn).
- All list/table UIs: mono micro-labels, right-aligned numeric columns,
`minmax(0, 1fr)` name columns, values ellipsize instead of wrapping.
diff --git a/README.md b/README.md
index 334051b..3904594 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-Your agents write everything down: transcripts, tool calls, files touched, spend. Then nobody reads it, and the context dies at each tool boundary. threadle reads it, draws it, and lets you wire it into the next run. Every [Claude Code](https://claude.com/claude-code), [opencode](https://opencode.ai), [Cursor](https://cursor.com) `agent`, [Antigravity](https://antigravity.google) `agy`, [Codex](https://github.com/openai/codex), [GitHub Copilot](https://github.com/features/copilot) CLI, and [Grok Build](https://x.ai/cli) session becomes a wireable node. Inspect what happened. Hand context across tools. Run the pipeline.
+Your agents write everything down: transcripts, tool calls, files touched, spend. Then nobody reads it, and the context dies at each tool boundary. threadle reads it, draws it, and lets you wire it into the next run. Every [Claude Code](https://claude.com/claude-code), [opencode](https://opencode.ai), [Cursor](https://cursor.com) `agent`, [Antigravity](https://antigravity.google) `agy`, [Codex](https://github.com/openai/codex), [GitHub Copilot](https://github.com/features/copilot) CLI, [Grok Build](https://x.ai/cli), and [Muse Code](https://dev.meta.ai) session becomes a wireable node. Inspect what happened. Hand context across tools. Run the pipeline.
**Yours, with no catch.** Discovery is read-only against storage you already have. threadle binds `127.0.0.1`, has no account and no telemetry, and does not upload transcripts. Pressing ▶ drives each tool’s own CLI under your credentials. If threadle disappears tomorrow, your sessions are exactly where they always were.
@@ -118,7 +118,7 @@ The full set lives in [`docs/screenshots/`](https://github.com/threadle-sh/threa
## Trust
-Parsers never mutate `~/.claude`, opencode’s SQLite, `~/.cursor`, `~/.gemini/antigravity-cli`, `~/.codex`, `~/.copilot`, or `~/.grok`. Runs spawn the official CLI, and *it* writes new sessions.
+Parsers never mutate `~/.claude`, opencode’s SQLite, `~/.cursor`, `~/.gemini/antigravity-cli`, `~/.codex`, `~/.copilot`, or `~/.grok`, `~/.muse`, or `~/.local/share/muse`. Runs spawn the official CLI, and *it* writes new sessions.
The server rejects foreign `Host` (DNS rebinding) and cross-site write `Origin` (CSRF). There is no remote mode and no auth token. List prices ship bundled (CI-refreshed from [models.dev](https://models.dev)). The process does not fetch them at runtime. Everything threadle persists is under `~/.config/threadle/`.
diff --git a/docs/cli/manual.md b/docs/cli/manual.md
index 6238095..e86244d 100644
--- a/docs/cli/manual.md
+++ b/docs/cli/manual.md
@@ -515,7 +515,7 @@ UI **▶** (no gates) and **≫** use the same `POST /api/run/workflow` path as
| `no skill named "…" found` | Install/unmute the skill, or mute the node in the UI and re-export |
| `nothing to run for "…"` | Path, recipe id, template id, or graph id wrong; run `threadle recipes` / `--list-templates` |
| `threadle server not reachable` | Start `threadle --no-open` before `jobs` / `services` / `skills` / `open` / `run --detach` |
-| Agent CLI not found | Install Cursor `agent` / Claude Code / opencode / Antigravity `agy` / Codex / GitHub Copilot / Grok Build; ensure on `PATH` — or run `threadle check` |
+| Agent CLI not found | Install Cursor `agent` / Claude Code / opencode / Antigravity `agy` / Codex / GitHub Copilot / Grok Build / Muse Code; ensure on `PATH` — or run `threadle check` |
| Wrong project files | Pass `--dir` to the repo root you intend |
| `context not materialized` / blocked ❝ | Wire a session/agent (extracts mid-run), drag from Library, or mute |
diff --git a/docs/provider-freshness.md b/docs/provider-freshness.md
index ba89ce2..317d85b 100644
--- a/docs/provider-freshness.md
+++ b/docs/provider-freshness.md
@@ -1,6 +1,6 @@
# Keeping providers fresh
-Agent CLIs (Claude Code, Cursor `agent`, opencode, Antigravity `agy`, Codex, GitHub Copilot, Grok Build) store transcripts in **undocumented** formats and rename flags without notice. threadle does **not** mirror their configs/APIs with a daily diff bot.
+Agent CLIs (Claude Code, Cursor `agent`, opencode, Antigravity `agy`, Codex, GitHub Copilot, Grok Build, Muse Code) store transcripts in **undocumented** formats and rename flags without notice. threadle does **not** mirror their configs/APIs with a daily diff bot.
## Strategy
@@ -33,7 +33,7 @@ It does **not** parse transcripts or prove schemas (that’s fixture CI).
### What `--providers` adds
-1. **Golden fixtures** (when `packages/server/test/fixtures/providers` is on disk — repo checkout / CI): parse one sample per provider (`fixture:claude-code`, `fixture:cursor`, … `fixture:grok`). Published installs skip these rows.
+1. **Golden fixtures** (when `packages/server/test/fixtures/providers` is on disk — repo checkout / CI): parse one sample per provider (`fixture:claude-code`, `fixture:cursor`, … `fixture:grok`, `fixture:muse`). Published installs skip these rows.
2. **Inject flag probes** for each **installed** CLI — run help and look for inject-critical tokens listed in [`packages/server/src/providers/freshness/inject-flags.ts`](../packages/server/src/providers/freshness/inject-flags.ts):
| Detail | Behavior |
@@ -41,13 +41,15 @@ It does **not** parse transcripts or prove schemas (that’s fixture CI).
| Missing binary | Skip (`ok`) |
| Help text contains any `match` string | `ok` |
| No match | `!!` and non-zero exit |
-| Subcommand help | Some probes use e.g. `opencode run --help` (not only top-level `--help`) |
+| Subcommand help | Some probes use e.g. `opencode run --help` or `muse exec --help` (not only top-level `--help`) |
| Bracket notation | Claude documents `--append-system-prompt[-file]` — both that form and the long flag count |
| Fixture missing | Skip (`ok`) |
| Fixture parse / shape fail | `!!` and non-zero exit |
A `!! flag:` row means “help no longer mentions a string we rely on for inject/run” — confirm with the real CLI before changing adapters (help text and the live flag can diverge). A `!! fixture:` row means a golden sample stopped normalizing.
+**Muse models.** Upstream has no `muse models` list yet. threadle’s model picker scrapes Spark ids from `muse exec --help` / `muse --help`, keeps hardcoded fallbacks, and honors `MUSE_MODELS`. That catalog is soft — expand fallbacks or env when Meta renames Spark ids.
+
## Adding a fixture
1. Anonymize one real transcript line (paths → `/tmp/…`, strip secrets).
diff --git a/docs/screenshots/canvas.png b/docs/screenshots/canvas.png
index 8f0a32b..6c9a44f 100644
Binary files a/docs/screenshots/canvas.png and b/docs/screenshots/canvas.png differ
diff --git a/docs/screenshots/sessions.png b/docs/screenshots/sessions.png
index 1ce15aa..b09f42d 100644
Binary files a/docs/screenshots/sessions.png and b/docs/screenshots/sessions.png differ
diff --git a/docs/screenshots/statistics.png b/docs/screenshots/statistics.png
index 964dea8..600df14 100644
Binary files a/docs/screenshots/statistics.png and b/docs/screenshots/statistics.png differ
diff --git a/docs/screenshots/stats.png b/docs/screenshots/stats.png
new file mode 100644
index 0000000..600df14
Binary files /dev/null and b/docs/screenshots/stats.png differ
diff --git a/package-lock.json b/package-lock.json
index eb6f7ae..604c3ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3912,6 +3912,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-in-ssh": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
+ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -4945,18 +4957,20 @@
}
},
"node_modules/open": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
- "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz",
+ "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==",
"license": "MIT",
"dependencies": {
- "default-browser": "^5.2.1",
+ "default-browser": "^5.5.1",
"define-lazy-prop": "^3.0.0",
+ "is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
- "wsl-utils": "^0.1.0"
+ "powershell-utils": "^0.2.1",
+ "wsl-utils": "^1.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -5253,6 +5267,18 @@
}
}
},
+ "node_modules/powershell-utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
+ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/pretty-ms": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz",
@@ -6881,15 +6907,28 @@
}
},
"node_modules/wsl-utils": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
- "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz",
+ "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==",
"license": "MIT",
"dependencies": {
- "is-wsl": "^3.1.0"
+ "is-wsl": "^3.1.0",
+ "powershell-utils": "^0.1.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wsl-utils/node_modules/powershell-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
+ "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -6935,7 +6974,7 @@
},
"packages/server": {
"name": "threadle",
- "version": "1.0.10",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.13.7",
@@ -6945,7 +6984,7 @@
"execa": "^9.5.2",
"gray-matter": "^4.0.3",
"hono": "^4.13.8",
- "open": "^10.1.0",
+ "open": "^11.0.4",
"smol-toml": "^1.8.0",
"zod": "^4.6.5"
},
@@ -6966,14 +7005,14 @@
},
"packages/shared": {
"name": "@threadle/shared",
- "version": "1.0.10",
+ "version": "1.1.1",
"dependencies": {
"zod": "^4.6.5"
}
},
"packages/web": {
"name": "@threadle/web",
- "version": "1.0.10",
+ "version": "1.1.1",
"dependencies": {
"@threadle/shared": "*",
"@vue-flow/background": "^1.3.2",
diff --git a/packages/server/package.json b/packages/server/package.json
index 5d3850b..e019969 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,7 +1,7 @@
{
"name": "threadle",
- "version": "1.0.11",
- "description": "Local-first node-graph patchbay for Claude Code, opencode, Cursor Agent, Antigravity, Codex, Copilot, and Grok Build sessions.",
+ "version": "1.1.1",
+ "description": "Local-first node-graph patchbay for Claude Code, opencode, Cursor Agent, Antigravity, Codex, Copilot, Grok Build and Muse sessions.",
"type": "module",
"license": "MIT",
"author": "Fabian Bienk (https://github.com/zFarbp)",
@@ -22,6 +22,7 @@
"codex",
"copilot",
"grok",
+ "muse",
"agent",
"workflow",
"patchbay"
@@ -54,7 +55,7 @@
"execa": "^9.5.2",
"gray-matter": "^4.0.3",
"hono": "^4.13.8",
- "open": "^10.1.0",
+ "open": "^11.0.4",
"smol-toml": "^1.8.0",
"zod": "^4.6.5"
},
diff --git a/packages/server/src/check.ts b/packages/server/src/check.ts
index f577c74..625ec1c 100644
--- a/packages/server/src/check.ts
+++ b/packages/server/src/check.ts
@@ -12,6 +12,7 @@ import { probeInjectFlags } from "./providers/freshness/probe.js";
import { probeGoldenFixtures } from "./providers/freshness/fixtures.js";
import { probeUpstream } from "./providers/freshness/upstream.js";
import { grokBin } from "./providers/grok/paths.js";
+import { museBin } from "./providers/muse/paths.js";
import { codexBin } from "./providers/codex/paths.js";
import { copilotBin } from "./providers/copilot/paths.js";
@@ -154,6 +155,7 @@ export async function runCheck(
checkPreferredOrPath("cli:codex", codexBin(), "codex"),
checkPreferredOrPath("cli:copilot", copilotBin(), "copilot"),
checkPreferredOrPath("cli:grok", grokBin(), "grok"),
+ checkPreferredOrPath("cli:muse", museBin(), "muse"),
]);
checks.push(...cliChecks);
diff --git a/packages/server/src/jobs.ts b/packages/server/src/jobs.ts
index 8da6503..0d15b47 100644
--- a/packages/server/src/jobs.ts
+++ b/packages/server/src/jobs.ts
@@ -19,6 +19,7 @@ export interface JobRecord {
label?: string;
/** workflow (graph) that launched the run, when known */
graphId?: string;
+ sessionRef?: { provider: string; sessionId: string };
status: JobStatus;
createdAt: number;
finishedAt?: number;
@@ -48,7 +49,10 @@ class JobRegistry extends EventEmitter {
kind: JobKind,
label?: string,
graphId?: string,
- opts?: { clientDriven?: boolean },
+ opts?: {
+ clientDriven?: boolean;
+ sessionRef?: { provider: string; sessionId: string };
+ },
): { jobId: string; signal: AbortSignal } {
this.counter += 1;
const jobId = `job_${Date.now().toString(36)}_${this.counter}`;
@@ -59,6 +63,7 @@ class JobRegistry extends EventEmitter {
kind,
label,
graphId,
+ sessionRef: opts?.sessionRef,
status: "running",
createdAt: Date.now(),
touchedAt: Date.now(),
diff --git a/packages/server/src/pilot-sessions/store.ts b/packages/server/src/pilot-sessions/store.ts
new file mode 100644
index 0000000..eeae56e
--- /dev/null
+++ b/packages/server/src/pilot-sessions/store.ts
@@ -0,0 +1,108 @@
+import fs from "node:fs";
+import path from "node:path";
+import { threadleConfigDir } from "../paths.js";
+
+export interface PilotSessionEntry {
+ provider: string;
+ sessionId: string;
+ markedAt: number;
+ caseId?: string;
+ tokensIn?: number;
+ tokensOut?: number;
+ promptEst?: number;
+ baselineEst?: number;
+ baselineNote?: string;
+ ignoreLocalMarkdown?: boolean;
+}
+
+interface PilotSessionsFile {
+ version: 1;
+ entries: PilotSessionEntry[];
+}
+
+function storePath(): string {
+ return path.join(threadleConfigDir(), "pilot-sessions.json");
+}
+
+export function pilotSessionKey(provider: string, sessionId: string): string {
+ return `${provider}:${sessionId}`;
+}
+
+async function readFile(): Promise {
+ try {
+ const raw = JSON.parse(
+ await fs.promises.readFile(storePath(), "utf8"),
+ ) as PilotSessionsFile;
+ if (raw?.version === 1 && Array.isArray(raw.entries)) return raw;
+ } catch {
+ // missing / corrupt -> empty
+ }
+ return { version: 1, entries: [] };
+}
+
+async function writeFile(data: PilotSessionsFile): Promise {
+ await fs.promises.mkdir(threadleConfigDir(), { recursive: true });
+ await fs.promises.writeFile(storePath(), JSON.stringify(data, null, 2), "utf8");
+}
+
+export async function markPilotSession(opts: {
+ provider: string;
+ sessionId: string;
+ caseId?: string;
+ tokensIn?: number;
+ tokensOut?: number;
+ promptEst?: number;
+ baselineEst?: number;
+ baselineNote?: string;
+ ignoreLocalMarkdown?: boolean;
+}): Promise {
+ const data = await readFile();
+ const key = pilotSessionKey(opts.provider, opts.sessionId);
+ const existing = data.entries.find(
+ (e) => pilotSessionKey(e.provider, e.sessionId) === key,
+ );
+ if (existing) {
+ if (opts.caseId && !existing.caseId) existing.caseId = opts.caseId;
+ if (opts.tokensIn != null) existing.tokensIn = opts.tokensIn;
+ if (opts.tokensOut != null) existing.tokensOut = opts.tokensOut;
+ if (opts.promptEst != null) existing.promptEst = opts.promptEst;
+ if (opts.baselineEst != null) existing.baselineEst = opts.baselineEst;
+ if (opts.baselineNote) existing.baselineNote = opts.baselineNote;
+ if (opts.ignoreLocalMarkdown != null) {
+ existing.ignoreLocalMarkdown = opts.ignoreLocalMarkdown;
+ }
+ await writeFile(data);
+ return existing;
+ }
+ const entry: PilotSessionEntry = {
+ provider: opts.provider,
+ sessionId: opts.sessionId,
+ markedAt: Date.now(),
+ caseId: opts.caseId,
+ tokensIn: opts.tokensIn,
+ tokensOut: opts.tokensOut,
+ promptEst: opts.promptEst,
+ baselineEst: opts.baselineEst,
+ baselineNote: opts.baselineNote,
+ ignoreLocalMarkdown: opts.ignoreLocalMarkdown,
+ };
+ data.entries.unshift(entry);
+ if (data.entries.length > 2000) data.entries.length = 2000;
+ await writeFile(data);
+ return entry;
+}
+
+export async function readPilotSessionMap(): Promise