@cordfuse/crosstalk 8.1.0 → 8.3.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/GUIDE-CLI.md +2 -2
- package/GUIDE-PROMPTS.md +4 -4
- package/README.md +8 -5
- package/commands/chat.js +1 -1
- package/lib/credentials.js +1 -1
- package/package.json +1 -4
- package/src/api.ts +3 -31
- package/src/dispatch.ts +1 -1
- package/src/invoke.ts +1 -1
- package/src/onboarding.ts +15 -0
- package/src/resolve.ts +1 -1
- package/src/web/dashboard.ts +1 -1
- package/src/web/layout.ts +13 -22
- package/src/web/stubs.ts +1 -1
- package/template/CROSSTALK.md +2 -2
- package/template/auth/README.md +17 -0
- package/template/data/crosstalk.yaml +17 -0
- package/src/web/chat-page.ts +0 -636
- package/src/web/chat-pty.ts +0 -254
package/GUIDE-CLI.md
CHANGED
|
@@ -120,7 +120,7 @@ Tabular list of saved profiles with their server URL + username + active marker.
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
PROFILE USERNAME SERVER
|
|
123
|
-
* default
|
|
123
|
+
* default alice http://127.0.0.1:7000
|
|
124
124
|
prod ops https://crosstalk.cordfuse.io
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -183,7 +183,7 @@ Write a primitive sub-message. `<body>` can be an inline string, a file path, or
|
|
|
183
183
|
```sh
|
|
184
184
|
crosstalk message send --to sonnet "summarise CROSSTALK.md"
|
|
185
185
|
crosstalk message send --to sonnet --fanout 3 --as junior-developer "draft a release codename"
|
|
186
|
-
crosstalk message send --to opus@
|
|
186
|
+
crosstalk message send --to opus@myhost --channel mctest "review these drafts" < drafts.md
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
Flags:
|
package/GUIDE-PROMPTS.md
CHANGED
|
@@ -8,7 +8,7 @@ The interactive Crosstalk pattern: **`crosstalk chat <agent>`** spawns the named
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
crosstalk chat claude
|
|
11
|
-
> ask sonnet on
|
|
11
|
+
> ask sonnet on myhost to summarise report.md
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
The agent CLI owns your terminal while running; Crosstalk owns the bus on disk; they meet at the transport git repo.
|
|
@@ -42,7 +42,7 @@ Say it however feels natural; these are the intents the interactive agent recogn
|
|
|
42
42
|
| You say… | What the agent does |
|
|
43
43
|
|---|---|
|
|
44
44
|
| "ask sonnet to summarise `report.md`" | `crosstalk message send --to sonnet "..."`, polls with `crosstalk message replies <path>`, surfaces the reply |
|
|
45
|
-
| "ask sonnet on
|
|
45
|
+
| "ask sonnet on myhost to look for typos" | `crosstalk message send --to sonnet@myhost "..."`, waits, reports back |
|
|
46
46
|
| "fan out three drafts running sonnet" | `crosstalk message send --to sonnet --fanout 3 "..."`, surfaces all three replies when they arrive |
|
|
47
47
|
| "in the *sprint-42* channel, ask opus to plan the work" | adds `--channel sprint-42` to the `message send` |
|
|
48
48
|
| "run this workflow" (with file path) | `crosstalk workflow run <file>` — the runtime auto-creates a child channel and runs compile → fanout → synthesize → route deterministically |
|
|
@@ -81,7 +81,7 @@ The interactive agent runs `crosstalk workflow compose "<your prose>"`. The engi
|
|
|
81
81
|
You see:
|
|
82
82
|
|
|
83
83
|
> *(running workflow…)*
|
|
84
|
-
> **workflow@
|
|
84
|
+
> **workflow@myhost:** *(final synthesis — the chosen unified release-notes draft.)*
|
|
85
85
|
|
|
86
86
|
You never see the orchestration — only the result. To drill into how the work happened, ask "show me the child channel for that workflow."
|
|
87
87
|
|
|
@@ -99,7 +99,7 @@ You never see the orchestration — only the result. To drill into how the work
|
|
|
99
99
|
|
|
100
100
|
## Tips for good results
|
|
101
101
|
|
|
102
|
-
- **Name the machine when it matters.** "ask sonnet" reaches sonnet on whichever dispatcher claims it first; "ask sonnet on
|
|
102
|
+
- **Name the machine when it matters.** "ask sonnet" reaches sonnet on whichever dispatcher claims it first; "ask sonnet on myhost" pins it to one machine. Use the machine when *where* the work runs is part of the ask.
|
|
103
103
|
- **Name the channel for threaded work.** If you don't, and there's more than one channel, the agent will ask which one rather than guess.
|
|
104
104
|
- **Be explicit for non-idempotent actions.** "deploy to staging" is a side effect; say so clearly and the model will verify it hasn't already happened.
|
|
105
105
|
- **You don't need to know UUIDs or paths — ever.** If the agent starts showing you raw file paths or git output, tell it to "talk to me like a person"; hiding that machinery is its job.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ The missing piece: a way to make **any** agent CLI talk to **any** other agent C
|
|
|
34
34
|
|
|
35
35
|
Crosstalk is that missing layer. The whole protocol is one idea — **a directory is the message bus.** Carry that directory with git (distributed, versioned, auditable) or a plain/shared filesystem (local, LAN, a mounted share) — same protocol either way.
|
|
36
36
|
|
|
37
|
-
- **Any CLI participates.** If an agent's CLI runs one prompt non-interactively and prints a reply, it's a valid model. Mix Claude Code, Codex, Gemini CLI, Qwen Code, opencode, Antigravity — or any future CLI that follows the same shape — in one transport.
|
|
37
|
+
- **Any CLI participates.** If an agent's CLI runs one prompt non-interactively and prints a reply, it's a valid model. Mix Claude Code, Codex, Gemini CLI, Qwen Code, opencode, Antigravity, GitHub Copilot CLI — or any future CLI that follows the same shape — in one transport.
|
|
38
38
|
- **Messages are files.** Every send is a markdown file with YAML frontmatter dropped into a channel. On the git transport each is a commit, so the whole conversation is git history — nothing to lose, nothing hidden, nothing to back up separately. On the filesystem transport it's just the files on disk.
|
|
39
39
|
- **Peer-to-peer.** Each machine runs its own crosstalk engine. No broker, no central runtime, no registry.
|
|
40
40
|
- **Multi-machine for free.** Git already solves "synchronize this across hosts"; a shared mount (SMB/NFS) does the same on a LAN. Crosstalk inherits whichever you point it at.
|
|
@@ -162,6 +162,11 @@ providers:
|
|
|
162
162
|
env_file: auth/openrouter.env # OPENAI_API_KEY=sk-or-... + OPENAI_BASE_URL=...
|
|
163
163
|
models:
|
|
164
164
|
qwen3-coder: qwen --auth-type openai --yolo --model qwen/qwen3-coder
|
|
165
|
+
|
|
166
|
+
github-copilot:
|
|
167
|
+
env_file: auth/github-copilot.env # COPILOT_GITHUB_TOKEN=github_pat_...
|
|
168
|
+
models:
|
|
169
|
+
copilot: copilot --yolo --silent -p
|
|
165
170
|
```
|
|
166
171
|
|
|
167
172
|
Per-agent install commands + the env vars each agent reads are documented in [transport/auth/README.md](transport/auth/README.md). For the auth schema design rationale, see [V8-AUTH-DESIGN.md](V8-AUTH-DESIGN.md).
|
|
@@ -186,13 +191,13 @@ Example `workflows/review-and-synthesize.md`:
|
|
|
186
191
|
```markdown
|
|
187
192
|
---
|
|
188
193
|
type: workflow
|
|
189
|
-
to: opus@
|
|
194
|
+
to: opus@desktop
|
|
190
195
|
as: orchestrator
|
|
191
196
|
---
|
|
192
197
|
|
|
193
198
|
1. Fan out 3 junior developers running sonnet@laptop. Each drafts the
|
|
194
199
|
proposal in their own voice.
|
|
195
|
-
2. Send the 3 drafts to a reviewer running opus@
|
|
200
|
+
2. Send the 3 drafts to a reviewer running opus@desktop to synthesize
|
|
196
201
|
into a single coherent proposal.
|
|
197
202
|
3. Return the synthesis to the original requester.
|
|
198
203
|
```
|
|
@@ -375,7 +380,6 @@ The engine serves a complete web UI on the same loopback port. After `crosstalk
|
|
|
375
380
|
| Route | Purpose |
|
|
376
381
|
|---|---|
|
|
377
382
|
| `/` | Dashboard — engine heartbeat, claimed models, channels, pending work, SSE live updates |
|
|
378
|
-
| `/chat` | Interactive chat panel — pick an agent CLI + cwd, attach via WebSocket (`/ws/chat`) to a fresh pty. `/exit` or the kill button SIGTERMs/SIGKILLs the agent. Async-by-design: no persistent sessions |
|
|
379
383
|
| `/c`, `/c/<handle>` | Channel list + threaded message view; send / rename / delete inline |
|
|
380
384
|
| `/w` | Workflow list + **compose form** (natural-language → `POST /api/workflows/compose` → editable YAML preview → `POST /api/workflows/submit`) |
|
|
381
385
|
| `/w/<childUuid>` | Live workflow detail with phase badge (compile / fanout / synthesize / complete / failed), per-dispatch + per-reply rows, completion banner, opt-in browser notification |
|
|
@@ -392,7 +396,6 @@ Off-host access (e.g., from a phone): the engine binds 127.0.0.1 only — front
|
|
|
392
396
|
|
|
393
397
|
- [`tsx`](https://www.npmjs.com/package/tsx) — run TypeScript directly without a build step (engine source lives under `src/`).
|
|
394
398
|
- [`yaml`](https://www.npmjs.com/package/yaml) — frontmatter and `data/crosstalk.yaml` parsing.
|
|
395
|
-
- [`ws`](https://www.npmjs.com/package/ws) + [`node-pty`](https://www.npmjs.com/package/node-pty) — WebSocket bridge for the `/chat` web panel (browser xterm.js ↔ host pty). Native build via `node-pty`'s prebuilds; falls back to source build on uncommon platforms.
|
|
396
399
|
|
|
397
400
|
---
|
|
398
401
|
|
package/commands/chat.js
CHANGED
|
@@ -26,7 +26,7 @@ import { spawn, spawnSync } from 'child_process';
|
|
|
26
26
|
import { requireInitialized } from '../lib/resolve.js';
|
|
27
27
|
import { flag, has, positionals } from '../lib/argv.js';
|
|
28
28
|
|
|
29
|
-
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
29
|
+
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy', 'copilot'];
|
|
30
30
|
|
|
31
31
|
function usage(exit = 0) {
|
|
32
32
|
const w = exit === 0 ? process.stdout : process.stderr;
|
package/lib/credentials.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Crosstalk — agent-agnostic swarm communication protocol over a shared directory (git or filesystem transport). Operator CLI + daemon in one binary.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,7 @@
|
|
|
31
31
|
"postpack": "rm -rf template"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@types/ws": "^8.18.1",
|
|
35
|
-
"node-pty": "^1.1.0",
|
|
36
34
|
"tsx": "^4.20.0",
|
|
37
|
-
"ws": "^8.21.0",
|
|
38
35
|
"yaml": "^2.8.0"
|
|
39
36
|
},
|
|
40
37
|
"devDependencies": {
|
package/src/api.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'http';
|
|
28
28
|
import { randomUUID } from 'crypto';
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
30
|
-
import { hostname
|
|
30
|
+
import { hostname } from 'os';
|
|
31
31
|
import { join } from 'path';
|
|
32
32
|
import { now, messageFilename } from './filenames.js';
|
|
33
33
|
import { parseFrontmatter, serializeFrontmatter } from './frontmatter.js';
|
|
@@ -46,8 +46,6 @@ import { wizardPage } from './web/wizard.js';
|
|
|
46
46
|
import { channelListPage, channelViewPage, agentsPage, type BusMessage } from './web/channels.js';
|
|
47
47
|
import { workflowsListPage, workflowDetailPage, type WorkflowSummary } from './web/workflows.js';
|
|
48
48
|
import { setupPage as authSetupPage, loginPage as authLoginPage } from './web/auth-pages.js';
|
|
49
|
-
import { chatPage } from './web/chat-page.js';
|
|
50
|
-
import { mountChatPty } from './web/chat-pty.js';
|
|
51
49
|
import { accountPage, usersPage, tokensPage, logsPage, settingsPage, aboutPage } from './web/stubs.js';
|
|
52
50
|
import { logBuffer, type LogEntry } from './log-buffer.js';
|
|
53
51
|
import { findOpenWorkflows, validatePlan, COMPILE_PROMPT, extractPlanFromOutput, type WorkflowPlan } from './workflow.js';
|
|
@@ -239,7 +237,7 @@ function handleListChannels(ctx: ApiContext): JsonRes {
|
|
|
239
237
|
// data/crosstalk.yaml entries whose CLI is on PATH (e.g. "sonnet", "haiku"
|
|
240
238
|
// — model names). /agents/installed lists raw CLI binaries (e.g.
|
|
241
239
|
// "claude" — the interactive entry point operators talk to).
|
|
242
|
-
const KNOWN_AGENT_BINARIES = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
240
|
+
const KNOWN_AGENT_BINARIES = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy', 'copilot'];
|
|
243
241
|
|
|
244
242
|
function handleAgentsInstalled(ctx: ApiContext): JsonRes {
|
|
245
243
|
const installed = KNOWN_AGENT_BINARIES.filter((bin) => isOnPath(bin));
|
|
@@ -666,7 +664,7 @@ async function tryWebAuthFlow(ctx: ApiContext, req: IncomingMessage, res: Server
|
|
|
666
664
|
// setup/api-setup path, redirect to /setup so the first browser visit
|
|
667
665
|
// bootstraps the admin account.
|
|
668
666
|
const HTML_GATED_PATHS = new Set([
|
|
669
|
-
'/', '/welcome', '/agents',
|
|
667
|
+
'/', '/welcome', '/agents',
|
|
670
668
|
'/tokens', '/logs', '/settings', '/account', '/users', '/about',
|
|
671
669
|
]);
|
|
672
670
|
if (method === 'GET' && (HTML_GATED_PATHS.has(path) || path.startsWith('/c') || path.startsWith('/w'))) {
|
|
@@ -1265,15 +1263,6 @@ async function tryHtmlRoute(ctx: ApiContext, req: IncomingMessage, res: ServerRe
|
|
|
1265
1263
|
return true;
|
|
1266
1264
|
}
|
|
1267
1265
|
|
|
1268
|
-
if (path === '/chat') {
|
|
1269
|
-
const installed = KNOWN_AGENT_BINARIES.filter((bin) => isOnPath(bin));
|
|
1270
|
-
writeHtml(res, 200, chatPage({
|
|
1271
|
-
host, alias: ctx.alias, installedAgents: installed, version: ctx.version,
|
|
1272
|
-
home: homedir(),
|
|
1273
|
-
}), ctx.version);
|
|
1274
|
-
return true;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
1266
|
// Auth-aware stub pages. authenticateRequest is a no-op (returns
|
|
1278
1267
|
// notOk) when the stores aren't configured — the gate above already
|
|
1279
1268
|
// short-circuited unauthed access, so by the time we get here we
|
|
@@ -1736,23 +1725,6 @@ export function startApi(ctx: ApiContext, opts: StartApiOptions): Server {
|
|
|
1736
1725
|
}
|
|
1737
1726
|
}
|
|
1738
1727
|
});
|
|
1739
|
-
// Mount the chat pty WS bridge. Same auth model as the HTML routes —
|
|
1740
|
-
// if user/token stores are configured, the WS upgrade is gated by a
|
|
1741
|
-
// valid crosstalk_session cookie (or bearer token). Without auth
|
|
1742
|
-
// stores (legacy/loopback-only mode), the WS is open since the bind
|
|
1743
|
-
// address is the security boundary.
|
|
1744
|
-
mountChatPty(server, {
|
|
1745
|
-
authorize: ctx.userStore && ctx.tokenStore
|
|
1746
|
-
? async (req) => {
|
|
1747
|
-
const auth = await authenticateRequest(req, {
|
|
1748
|
-
tokenStore: ctx.tokenStore!,
|
|
1749
|
-
userStore: ctx.userStore!,
|
|
1750
|
-
});
|
|
1751
|
-
return auth.ok;
|
|
1752
|
-
}
|
|
1753
|
-
: undefined,
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
1728
|
server.listen(port, bind, () => {
|
|
1757
1729
|
opts.log('api_listening', { host: bind, port });
|
|
1758
1730
|
});
|
package/src/dispatch.ts
CHANGED
|
@@ -294,7 +294,7 @@ async function runActivationPass(
|
|
|
294
294
|
// Normalize asker identity the same way self-suppression does: strip
|
|
295
295
|
// the @machine suffix before comparing against actorName. Without
|
|
296
296
|
// this, an actor that sent a sub-message never wakes for its reply,
|
|
297
|
-
// because asker='alice@
|
|
297
|
+
// because asker='alice@myhost' never matches actorName='alice'. The
|
|
298
298
|
// bug was masked by the workflow smoke test, which used direct
|
|
299
299
|
// stdout replies — fan-out + fan-in is the case that surfaces it.
|
|
300
300
|
const senderOf = (relPath: string) => {
|
package/src/invoke.ts
CHANGED
|
@@ -174,7 +174,7 @@ export function formatBatchedUserMessage(msgs: ChannelMessage[]): string {
|
|
|
174
174
|
export interface ReplyOpts {
|
|
175
175
|
transportRoot: string;
|
|
176
176
|
channelUuid: string;
|
|
177
|
-
fromModel: string; // e.g. "sonnet@
|
|
177
|
+
fromModel: string; // e.g. "sonnet@myhost"
|
|
178
178
|
to: string; // the requester
|
|
179
179
|
re: string | string[];
|
|
180
180
|
body: string;
|
package/src/onboarding.ts
CHANGED
|
@@ -61,6 +61,21 @@ export const AGENT_CATALOG: Record<string, AgentCatalogEntry> = {
|
|
|
61
61
|
keyEnv: null,
|
|
62
62
|
keyHint: 'OpenCode uses its own provider config — no key needed here.',
|
|
63
63
|
},
|
|
64
|
+
// Binary is `copilot` (npm: @github/copilot) — NOT `gh copilot`, a
|
|
65
|
+
// separate thin launcher bundled with the gh CLI that downloads and
|
|
66
|
+
// runs a different copy of the same binary from a different install
|
|
67
|
+
// path; KNOWN_AGENT_BINARIES in api.ts checks for the real one. -p
|
|
68
|
+
// must stay the LAST token in `command` — it takes an explicit value
|
|
69
|
+
// (like claude/gemini's -p/--print here), not a bare trailing
|
|
70
|
+
// positional, so the dispatcher's appended message body becomes its
|
|
71
|
+
// argument. --yolo skips the tool/path/URL confirmation prompts;
|
|
72
|
+
// --silent keeps stdout to just the reply text.
|
|
73
|
+
copilot: {
|
|
74
|
+
cli: 'copilot', label: 'GitHub Copilot CLI', provider: 'github-copilot', model: 'copilot',
|
|
75
|
+
command: 'copilot --yolo --silent -p',
|
|
76
|
+
keyEnv: 'COPILOT_GITHUB_TOKEN',
|
|
77
|
+
keyHint: 'Leave blank if `gh` is already logged in on this host; otherwise paste a fine-grained PAT with the "Copilot Requests" permission.',
|
|
78
|
+
},
|
|
64
79
|
};
|
|
65
80
|
|
|
66
81
|
export interface ConnectResult {
|
package/src/resolve.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function formatResolutionError(r: Exclude<ResolutionResult, { kind: 'ok'
|
|
|
75
75
|
|
|
76
76
|
// Resolve a `to:` field value that may carry an `@host` suffix. The host
|
|
77
77
|
// suffix is split off, the actor part resolved, then re-attached. This
|
|
78
|
-
// lets workflow scoping (`model@
|
|
78
|
+
// lets workflow scoping (`model@myhost`) work uniformly with bare or
|
|
79
79
|
// qualified model references.
|
|
80
80
|
export interface ToResolution {
|
|
81
81
|
kind: 'ok' | 'unknown' | 'ambiguous' | 'reserved';
|
package/src/web/dashboard.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function dashboardPage(d: DashboardData): string {
|
|
|
47
47
|
} else if (d.channels.length === 0) {
|
|
48
48
|
stColor = '#7ee787';
|
|
49
49
|
stLabel = `Ready · ${claimed} agent${claimed === 1 ? '' : 's'} connected`;
|
|
50
|
-
stHint = 'Create a channel to start a conversation
|
|
50
|
+
stHint = 'Create a channel to start a conversation.';
|
|
51
51
|
} else {
|
|
52
52
|
stColor = '#7ee787';
|
|
53
53
|
stLabel = `Live · ${claimed} agent${claimed === 1 ? '' : 's'}, ${d.channels.length} channel${d.channels.length === 1 ? '' : 's'}`;
|
package/src/web/layout.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// sky-blue accents, monospace) so a single Cordfuse operator sees a
|
|
5
5
|
// consistent visual family across tools. Layout is async-by-design:
|
|
6
6
|
// dashboards, channel browser, threaded message view, dispatcher
|
|
7
|
-
// status.
|
|
8
|
-
//
|
|
7
|
+
// status. (An interactive WS+pty chat panel existed briefly and was
|
|
8
|
+
// removed — redundant with @cordfuse/llmux, which does this with a
|
|
9
|
+
// real tracked/tmux-backed session model; crosstalk stays async-only.)
|
|
9
10
|
|
|
10
11
|
const BRAND_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="90" ry="90" fill="#0b0c10"/><rect x="1.5" y="1.5" width="509" height="509" rx="89" ry="89" fill="none" stroke="#7cc4ff" stroke-width="1.5" stroke-opacity="0.22"/><text x="256" y="236" text-anchor="middle" dominant-baseline="central" font-family="'Noto Sans Mono', 'Courier New', monospace" font-size="155" font-weight="700" fill="#7cc4ff" letter-spacing="-3">{Ct}</text></svg>`;
|
|
11
12
|
const FAVICON_DATA_URL = `data:image/svg+xml,${encodeURIComponent(BRAND_SVG)}`;
|
|
@@ -37,7 +38,6 @@ interface NavItem {
|
|
|
37
38
|
|
|
38
39
|
const NAV: NavItem[] = [
|
|
39
40
|
{ id: 'dashboard', label: 'Dashboard', icon: '▦', href: '/' },
|
|
40
|
-
{ id: 'chat', label: 'Chat', icon: '◉', href: '/chat' },
|
|
41
41
|
{ id: 'channels', label: 'Channels', icon: '⇄', href: '/c' },
|
|
42
42
|
{ id: 'workflows', label: 'Workflows', icon: '⎇', href: '/w' },
|
|
43
43
|
{ id: 'tokens', label: 'Tokens', icon: '⚿', href: '/tokens' },
|
|
@@ -74,18 +74,15 @@ function commonStyles(): string {
|
|
|
74
74
|
body{padding:18px 16px 80px;max-width:980px;margin:0 auto;box-sizing:border-box}
|
|
75
75
|
a{color:#7cc4ff;text-decoration:none}
|
|
76
76
|
a:hover{text-decoration:underline}
|
|
77
|
-
header{display:flex;
|
|
78
|
-
|
|
79
|
-
h1{font-size:18px;margin:0}
|
|
77
|
+
header{display:flex;align-items:center;gap:10px;margin-bottom:18px;background:#1c2128;border:1px solid #262c34;border-radius:8px;padding:10px 14px}
|
|
78
|
+
h1{font-size:16px;margin:0;min-width:0;flex:1 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
80
79
|
h1 .brand{color:#7cc4ff;letter-spacing:.08em;font-weight:600}
|
|
81
|
-
h1 .host{color:#a371f7;font-weight:500;margin-left:8px}
|
|
82
80
|
h1 .page-title{color:#a371f7;font-weight:500;margin-left:8px}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
#nav-toggle{background:#
|
|
88
|
-
#nav-toggle:hover{background:#252b34;border-color:#3a414b}
|
|
81
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#7ee787;flex:0 0 auto}
|
|
82
|
+
.dot.stale{background:#d29922}
|
|
83
|
+
.dot.dead{background:#f85149}
|
|
84
|
+
#nav-toggle{background:#1c2128;color:#e6e8eb;border:none;border-radius:6px;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;font-size:18px;line-height:1;margin-right:10px;flex:0 0 auto;transition:background 150ms ease}
|
|
85
|
+
#nav-toggle:hover{background:#252b34}
|
|
89
86
|
#nav-toggle:active{transform:scale(.94)}
|
|
90
87
|
#nav-drawer{position:fixed;top:0;left:-300px;width:280px;height:100dvh;background:#0e1116;border-right:1px solid #1f2329;transition:left 220ms ease;z-index:55;padding:18px 0;box-sizing:border-box;display:flex;flex-direction:column}
|
|
91
88
|
#nav-drawer.open{left:0}
|
|
@@ -226,15 +223,9 @@ export function page(opts: PageOpts): string {
|
|
|
226
223
|
</head><body>
|
|
227
224
|
${navDrawer({ host: opts.host, alias: opts.alias, ...(opts.activeNav ? { activeId: opts.activeNav } : {}) })}
|
|
228
225
|
<header>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<span class="dot" id="liveness-dot"></span>
|
|
233
|
-
<span id="liveness-label">live · 5s</span>
|
|
234
|
-
<span>·</span>
|
|
235
|
-
<span>alias ${escapeHtml(opts.alias)}</span>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
226
|
+
<button id="nav-toggle" aria-label="open navigation" type="button">≡</button>
|
|
227
|
+
<h1><span class="brand">CROSSTALK</span>${titleSuffix}</h1>
|
|
228
|
+
<span class="dot" id="liveness-dot" title="${escapeHtml(opts.host)} · alias ${escapeHtml(opts.alias)}"></span>
|
|
238
229
|
</header>
|
|
239
230
|
<main>
|
|
240
231
|
${opts.body}
|
package/src/web/stubs.ts
CHANGED
|
@@ -267,7 +267,7 @@ export function tokensPage(opts: CommonOpts & { tokens: IdentityToken[]; viewerU
|
|
|
267
267
|
<form id="mint-form" autocomplete="off">
|
|
268
268
|
<div class="field">
|
|
269
269
|
<label for="tk-name">token name</label>
|
|
270
|
-
<input type="text" id="tk-name" required placeholder="e.g.
|
|
270
|
+
<input type="text" id="tk-name" required placeholder="e.g. laptop-cli, workstation-ci">
|
|
271
271
|
</div>
|
|
272
272
|
<div class="actions">
|
|
273
273
|
<button type="submit" class="primary">Mint</button>
|
package/template/CROSSTALK.md
CHANGED
|
@@ -72,7 +72,7 @@ Every message is a markdown file with YAML frontmatter:
|
|
|
72
72
|
```
|
|
73
73
|
---
|
|
74
74
|
from: alice
|
|
75
|
-
to: sonnet@
|
|
75
|
+
to: sonnet@myhost
|
|
76
76
|
timestamp: 2026-06-11T19:00:00.000Z
|
|
77
77
|
---
|
|
78
78
|
|
|
@@ -223,7 +223,7 @@ The model never sees state machinery: each invocation is a single-turn content t
|
|
|
223
223
|
The `to:` field accepts:
|
|
224
224
|
|
|
225
225
|
- `to: sonnet` — bare model name. Any dispatcher claiming `sonnet` may pick it up; first-grab wins (via git push race on the git transport, via collision-free filenames on the filesystem transport).
|
|
226
|
-
- `to: sonnet@
|
|
226
|
+
- `to: sonnet@myhost` — narrowed to the dispatcher whose `--alias` is `myhost`.
|
|
227
227
|
|
|
228
228
|
The model name is everything before the `@`; the machine alias is everything after. The `re:` activation rule ignores `@machine` suffixes — only addressing honors them.
|
|
229
229
|
|
package/template/auth/README.md
CHANGED
|
@@ -144,6 +144,23 @@ Then `codex login --with-api-key` in that HOME gives you a second account, isola
|
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
+
## copilot (GitHub Copilot CLI)
|
|
148
|
+
|
|
149
|
+
Binary is `copilot` (`npm install -g @github/copilot`) — **not** `gh copilot`, a separate thin launcher bundled with the `gh` CLI that downloads and runs a different copy of the same binary from a different install path. Confirmed live: `gh copilot`'s own install-detection path (`~/.local/share/gh/copilot`) and the real npm install (`copilot` on `$PATH`) are unrelated locations.
|
|
150
|
+
|
|
151
|
+
**Headless auth env** (checked in this order): `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`. Use a fine-grained personal access token with the "Copilot Requests" permission (github.com/settings/personal-access-tokens/new — classic `ghp_` tokens are not supported), or an OAuth token already stored by `gh auth login` / `copilot login`.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
# auth/github-copilot.env
|
|
155
|
+
COPILOT_GITHUB_TOKEN=github_pat_...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Required CLI flags:** `copilot --yolo --silent -p` — `--yolo` skips the tool/path/URL confirmation prompts (documented equivalent of `--allow-all-tools --allow-all-paths --allow-all-urls`); `--silent` suppresses the trailing usage-stats line so stdout is exactly the reply text; `-p` **must be the last token** in the command string so the runtime's appended message body becomes its value — unlike the other agents' `-p`/`--print`-style flags, this one takes an explicit value rather than reading a bare trailing positional (see Command contract above).
|
|
159
|
+
|
|
160
|
+
**No folder-trust prompt in this mode.** The interactive TUI (what llmux drives) gates every spawn in a new directory behind a folder-trust confirmation with no flag to skip it. Verified live that headless `-p` mode doesn't have this gate at all, in both a plain directory and a git repo — no wrapper or workaround needed here, unlike the interactive case.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
147
164
|
## qwen (Qwen Code CLI)
|
|
148
165
|
|
|
149
166
|
Multi-provider via `--auth-type`. The OpenAI-compatible path is the cleanest headless route:
|
|
@@ -153,6 +153,23 @@ providers:
|
|
|
153
153
|
# models:
|
|
154
154
|
# codex: codex --skip-git-repo-check
|
|
155
155
|
|
|
156
|
+
# ── GitHub Copilot CLI ─────────────────────────────────────────────
|
|
157
|
+
# Binary is `copilot` (npm: @github/copilot) — NOT `gh copilot`, a
|
|
158
|
+
# separate thin launcher bundled with the gh CLI that downloads and
|
|
159
|
+
# runs a different copy of the same binary from a different path.
|
|
160
|
+
# Headless auth env (checked in order): COPILOT_GITHUB_TOKEN, GH_TOKEN,
|
|
161
|
+
# GITHUB_TOKEN — a fine-grained PAT with the "Copilot Requests"
|
|
162
|
+
# permission, or an OAuth token from `gh auth login` / `copilot login`.
|
|
163
|
+
# -p must be the LAST token here (unlike a bare trailing positional,
|
|
164
|
+
# it takes an explicit value — see COMMAND CONTRACT above). No
|
|
165
|
+
# folder-trust prompt in this mode, verified in both a plain
|
|
166
|
+
# directory and a git repo — see transport/auth/README.md for detail.
|
|
167
|
+
#
|
|
168
|
+
# github-copilot:
|
|
169
|
+
# env_file: auth/github-copilot.env
|
|
170
|
+
# models:
|
|
171
|
+
# copilot: copilot --yolo --silent -p
|
|
172
|
+
|
|
156
173
|
# ── OpenRouter (gateway → many models) ─────────────────────────────
|
|
157
174
|
# OpenAI-compatible gateway. Use with codex, qwen, opencode (native),
|
|
158
175
|
# or any other OpenAI-SDK-shaped client.
|