@usenexus/cli 0.0.2 → 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.
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="referrer" content="no-referrer" />
7
+ <title>nexus</title>
8
+ <script type="module" crossorigin src="./assets/index-DycaEuDy.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-Bj0JERnz.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
package/package.json CHANGED
@@ -1,13 +1,51 @@
1
1
  {
2
2
  "name": "@usenexus/cli",
3
- "version": "0.0.2",
4
- "description": "nexus: a repo that ships itself. npx @usenexus/cli launches the local GUI (coming soon).",
3
+ "version": "0.1.0",
4
+ "description": "nexus: a local GUI for Claude Code that runs the nexus loop on your repo. npx @usenexus/cli",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "bin": { "nexus": "bin/nexus.mjs" },
8
- "files": ["bin", "README.md", "LICENSE"],
9
- "engines": { "node": ">=20" },
10
- "repository": { "type": "git", "url": "git+https://github.com/daretodave/nexus.git" },
7
+ "bin": {
8
+ "nexus": "bin/nexus.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "server",
13
+ "shared",
14
+ "dist",
15
+ "prompts",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "scripts": {
23
+ "build": "node scripts/bundle-prompts.mjs && vite build",
24
+ "test": "node --test \"test/*.test.mjs\"",
25
+ "start": "node bin/nexus.mjs",
26
+ "prepublishOnly": "npm run build && npm test"
27
+ },
28
+ "dependencies": {
29
+ "@anthropic-ai/claude-agent-sdk": "0.3.305"
30
+ },
31
+ "devDependencies": {
32
+ "@vitejs/plugin-react": "6.1.2",
33
+ "react": "19.3.1",
34
+ "react-dom": "19.3.1",
35
+ "vite": "8.3.4"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/daretodave/nexus.git",
40
+ "directory": "packages/cli"
41
+ },
11
42
  "homepage": "https://github.com/daretodave/nexus#readme",
12
- "keywords": ["claude-code", "nexus", "autonomous", "agents", "methodology"]
43
+ "keywords": [
44
+ "claude-code",
45
+ "nexus",
46
+ "autonomous",
47
+ "agents",
48
+ "methodology",
49
+ "gui"
50
+ ]
13
51
  }
@@ -0,0 +1,119 @@
1
+ # Adopt nexus — agent prompt
2
+
3
+ > Clone nexus next to your repo, then paste the short version
4
+ > from README.md's
5
+ > [TL;DR — clone + delegate the adoption](../README.md#tldr--clone--delegate-the-adoption)
6
+ > into your agent — it points here. This file is the full
7
+ > instruction set the agent then follows; read it yourself
8
+ > first if you want to know exactly what you're authorizing.
9
+ >
10
+ > Use this if you already have a `spec.md`. If you only have a
11
+ > verbal pitch, use [`pitch.md`](./pitch.md) instead.
12
+
13
+ ---
14
+
15
+ ## The prompt
16
+
17
+ ```
18
+ Adopt nexus.
19
+
20
+ nexus is at ../nexus (or ./.nexus if submoduled). It is a
21
+ methodology + template kit for turning this repo into an
22
+ autonomous-loop project. Read the following, in order, before
23
+ making a single change:
24
+
25
+ 1. ../nexus/README.md — entry point
26
+ 2. ../nexus/concepts/architecture.md — the whole system
27
+ 3. ../nexus/concepts/skills-anatomy.md — how skills work
28
+ 4. ../nexus/playbooks/new-project.md AND
29
+ ../nexus/playbooks/existing-project.md
30
+ — pick the one that applies
31
+ 4a. ../nexus/playbooks/polyrepo.md AND/OR
32
+ ../nexus/playbooks/workspace.md
33
+ — only if the topology check
34
+ under "Then:" below (in
35
+ this same prompt) lands on
36
+ either
37
+ 5. ../nexus/playbooks/ci-providers.md — for the deploy gate
38
+ 6. ../nexus/intervention-spectrum.md — how the loop scales
39
+ 7. ../nexus/customization/*.md — e.g. verify gate, hermetic
40
+ e2e, data layer, sub-agents;
41
+ skim the rest as needed
42
+
43
+ Then:
44
+
45
+ - Decide whether this is greenfield or brownfield by reading
46
+ `git log --oneline | wc -l`, looking at the file tree, and
47
+ checking for an existing spec.md.
48
+ - Check the topology: is there already a sibling `plan/`
49
+ directory next to this repo, or multiple sibling repos
50
+ under one un-versioned root? Infer a default from that
51
+ (single-repo if neither), but always confirm it with the
52
+ user before proceeding — see the ask list below. Getting
53
+ this wrong reshapes file locations from phase 1 onward.
54
+ - Follow the matching playbook end-to-end. Do not skip steps.
55
+ - Copy templates from ../nexus/templates/ into this repo.
56
+ - Replace placeholders (<PROJECT>, <PROJECT_LOWER>,
57
+ <PROJECT_TAGLINE>, <HOSTING_URL>, <HOSTING_PROVIDER>,
58
+ <REPO_SLUG>, <DEFAULT_BRANCH>, <PROJECT_PKG_PREFIX>) with
59
+ values you derive from the existing repo state. If a value
60
+ is genuinely unknowable, surface it in plan/AUDIT.md as a
61
+ [needs-user-call] row and continue with a defensible
62
+ default.
63
+ - Ask the user ONLY for: (a) the hosting provider name and
64
+ auth token if not visibly configured, (b) the project's
65
+ canonical name + tagline if no spec exists, (c) the
66
+ URL/API/CLI contract if it cannot be inferred, (d)
67
+ confirmation of the topology guess above — single-repo,
68
+ polyrepo (`plan/` split into a sibling repo), or workspace
69
+ (3+ sibling repos under one un-versioned root). For
70
+ everything else, decide.
71
+ - Adapt bearings.md to reflect the actual stack present in
72
+ this repo. Do not assume Next.js, Tailwind, or any
73
+ specific framework — read package.json / Cargo.toml /
74
+ pyproject.toml / go.mod and adapt.
75
+ - Write the plan as specs and chunks (nexus/concepts/
76
+ plan-shape.md): a plan/README.md roadmap with one row per
77
+ spec drawn from the spec (or from CURRENT-STATE.md for
78
+ brownfield), the first spec or two chunked (S/M/L, every
79
+ acceptance ID covered), the rest `Not chunked yet.`.
80
+ Copy templates/plan/nexus.lock and run
81
+ scripts/plan-check.mjs until it is green.
82
+ - Wire the verify gate against this repo's actual test
83
+ setup. Wire the deploy gate against the chosen provider.
84
+ - At the end: produce a single commit titled
85
+ "chore: adopt nexus methodology" with a body listing every
86
+ file added/modified, every placeholder you resolved, and
87
+ every [needs-user-call] you logged. Push.
88
+ - Then stop. Do not invoke /ship-a-chunk yourself; let the
89
+ user do that as the first conscious step.
90
+
91
+ Standing rules carried from agents.md:
92
+ - Commit and push as a single atomic act.
93
+ - No Co-Authored-By trailers, no emojis. If your harness
94
+ auto-appends one of these by default, strip it before every
95
+ commit — this rule overrides that convention here.
96
+ - No --no-verify, no force-push, no destructive resets.
97
+ - Tests alongside code.
98
+ - When in doubt: decide, document the call in the commit body,
99
+ proceed.
100
+
101
+ Estimated time: well under an hour, agent-paced (no
102
+ read-and-decide pauses) — new-project.md's own header quotes
103
+ 2–3 hours for a human working the same playbook manually and
104
+ notes the delegated-agent case finishes faster; see its note
105
+ on the difference. Begin.
106
+ ```
107
+
108
+ ---
109
+
110
+ ## See also
111
+
112
+ - [`README.md`](../README.md) — the short paste that points
113
+ here, plus what to expect when the agent returns and when
114
+ this path is the wrong one.
115
+ - [`playbooks/new-project.md`](../playbooks/new-project.md) /
116
+ [`playbooks/existing-project.md`](../playbooks/existing-project.md)
117
+ — the playbooks this prompt tells the agent to follow.
118
+ - [`pitch.md`](./pitch.md) — the pre-spec variant, for when
119
+ there's no `spec.md` yet.
@@ -0,0 +1,93 @@
1
+ # Pitch-to-adopted — agent prompt
2
+
3
+ > Clone nexus next to your repo, then paste the short version
4
+ > from README.md's
5
+ > [TL;DR — I have a pitch, no spec yet](../README.md#tldr--i-have-a-pitch-no-spec-yet)
6
+ > into your agent — it points here, with your pitch inline.
7
+ > This file is the full instruction set the agent then
8
+ > follows; read it yourself first if you want to know exactly
9
+ > what you're authorizing.
10
+ >
11
+ > Use this if you have a working name and a one-paragraph
12
+ > pitch, but no `spec.md` yet. If you already have a `spec.md`,
13
+ > use [`adopt.md`](./adopt.md) instead.
14
+
15
+ ---
16
+
17
+ ## The prompt
18
+
19
+ ```
20
+ Run nexus's pitch-to-adopted flow.
21
+
22
+ nexus is at ../nexus (or ./.nexus if submoduled). The plan
23
+ is: pre-spec interview → write spec.md → adopt nexus.
24
+
25
+ Phase A — pre-spec interview (30-45 minutes)
26
+
27
+ Read ../nexus/playbooks/pre-spec.md end-to-end. Read
28
+ ../nexus/concepts/asking-well.md for question shape. Then
29
+ run the three question batches (foundation, spine, surface)
30
+ per the playbook. AskUserQuestion is allowed during this
31
+ phase — it's pre-spec, not in-skill. Once spec.md is
32
+ committed at the end of Phase A, that rule reverts.
33
+
34
+ Here's my pitch in my own words:
35
+
36
+ <ONE-PARAGRAPH PITCH — what the product is, who it's for,
37
+ what makes it interesting. Don't polish.>
38
+
39
+ Phase A deliverables (committed before Phase B starts):
40
+ - spec.md (at least one page, persona named, v1 scope)
41
+ - plan/bearings.md stub (Surface, Auth, Stack, hosting,
42
+ voice, hard rules)
43
+ - (optional) claude-design.prompt.md if Batch 3 Q2 landed
44
+ on "commission a visual system" (see
45
+ ../nexus/customization/visual-system.md)
46
+ - (optional) NEXUS_LESSONS.md scratch — capture nexus gaps
47
+ you hit during the interview, for a later /lessons-pr
48
+ pass back to the nexus repo
49
+
50
+ Phase B — adoption
51
+
52
+ Once Phase A's commits land, switch to the standard adoption
53
+ prompt: read and follow ../nexus/prompts/adopt.md exactly,
54
+ including its own narrow four-item ask list (hosting
55
+ provider/token, project name/tagline, URL/API/CLI contract,
56
+ topology confirmation) — that list is adopt.md's own
57
+ carve-out, the same class as /oversight's. Beyond those four
58
+ items, AskUserQuestion is not allowed. Decide and document;
59
+ don't ask.
60
+
61
+ End-state:
62
+ - chore: adopt nexus methodology commit landed and pushed
63
+ - Day 1 checklist in new-project.md passes
64
+ - Ready for /ship-a-chunk as the first conscious step.
65
+ - Do NOT invoke /ship-a-chunk yourself; let the user do
66
+ that.
67
+
68
+ Standing rules:
69
+ - Commit and push as a single atomic act per logical chunk.
70
+ - No Co-Authored-By trailers, no emojis.
71
+ - No --no-verify, no force-push.
72
+
73
+ Estimated time: 60-90 minutes total, agent-paced for Phase B
74
+ (Phase A is genuinely interactive, per pre-spec.md's own
75
+ 30-45 minute estimate; Phase B's 2-3 hour playbook estimate
76
+ is the human-paced figure — see new-project.md's note).
77
+ Begin with Phase A.
78
+ ```
79
+
80
+ Pitch quality matters. If you can't describe the product in
81
+ one paragraph in plain language, come back when you can. The
82
+ agent will not invent product direction for you.
83
+
84
+ ---
85
+
86
+ ## See also
87
+
88
+ - [`README.md`](../README.md) — the short paste that points
89
+ here.
90
+ - [`adopt.md`](./adopt.md) — the Phase B prompt this one
91
+ hands off to.
92
+ - [`playbooks/pre-spec.md`](../playbooks/pre-spec.md) — what
93
+ Phase A tells the agent to follow.
package/server/gh.mjs ADDED
@@ -0,0 +1,89 @@
1
+ // GitHub Actions setup and live runs, through the person's own `gh` login.
2
+ // Run state is observed only (gh run list / view); nothing here infers
3
+ // progress from a clock. A run still queued after STALL_MS reads "stalled".
4
+ import { run, json } from './sh.mjs';
5
+
6
+ export const STALL_MS = 2 * 60 * 1000;
7
+ export const POLL_MS = 8000;
8
+ export const SECRET = 'CLAUDE_CODE_OAUTH_TOKEN';
9
+
10
+ export async function actionsSetup(repo) {
11
+ const auth = await run('gh', ['auth', 'status'], { cwd: repo });
12
+ if (!auth.ok) return { gh: false, note: auth.missing ? 'gh is not installed.' : 'gh is not signed in. Run `gh auth login`.' };
13
+ const [secrets, workflows, perms, rulesets] = await Promise.all([
14
+ run('gh', ['secret', 'list', '--json', 'name'], { cwd: repo }),
15
+ run('gh', ['workflow', 'list', '--all', '--json', 'id,name,path,state'], { cwd: repo }),
16
+ run('gh', ['api', 'repos/{owner}/{repo}/actions/permissions/workflow'], { cwd: repo }),
17
+ run('gh', ['api', 'repos/{owner}/{repo}/rulesets'], { cwd: repo }),
18
+ ]);
19
+ const names = (json(secrets.stdout) || []).map((s) => s.name);
20
+ const p = json(perms.stdout) || {};
21
+ const rs = json(rulesets.stdout);
22
+ return {
23
+ gh: true,
24
+ steps: [
25
+ {
26
+ id: 'secret',
27
+ label: `${SECRET} repo secret`,
28
+ ok: names.includes(SECRET),
29
+ detail: secrets.ok ? (names.length ? `secrets: ${names.join(', ')}` : 'no repo secrets') : 'could not list secrets (needs admin on the repo)',
30
+ fix: `Run \`claude setup-token\`, then \`gh secret set ${SECRET}\` and paste the token.`,
31
+ },
32
+ {
33
+ id: 'permissions',
34
+ label: 'Workflow token can write',
35
+ ok: p.default_workflow_permissions === 'write',
36
+ detail: perms.ok ? `default: ${p.default_workflow_permissions || 'unknown'}; may approve PRs: ${p.can_approve_pull_request_reviews ? 'yes' : 'no'}` : 'could not read workflow permissions',
37
+ fix: 'Settings -> Actions -> General -> Workflow permissions: Read and write.',
38
+ },
39
+ {
40
+ id: 'rulesets',
41
+ label: 'Rulesets on main and claims',
42
+ ok: Array.isArray(rs) && rs.length > 0,
43
+ optional: true,
44
+ detail: Array.isArray(rs) ? (rs.length ? rs.map((r) => `${r.name} (${r.enforcement})`).join(', ') : 'none') : 'could not read rulesets',
45
+ fix: 'Optional in owner mode. In PR mode, protect main so only pull requests land.',
46
+ },
47
+ ],
48
+ workflows: (json(workflows.stdout) || []).map((w) => ({ id: w.id, name: w.name, path: w.path, enabled: w.state === 'active', state: w.state })),
49
+ };
50
+ }
51
+
52
+ export async function setWorkflow(repo, id, enabled) {
53
+ if (!/^[0-9]+$/.test(String(id))) return { ok: false, error: 'bad workflow id' };
54
+ const r = await run('gh', ['workflow', enabled ? 'enable' : 'disable', String(id)], { cwd: repo });
55
+ return { ok: r.ok, out: (r.stdout + r.stderr).trim() };
56
+ }
57
+
58
+ export async function listRuns(repo) {
59
+ const r = await run('gh', ['run', 'list', '-L', '20', '--json', 'databaseId,displayTitle,workflowName,status,conclusion,createdAt,updatedAt,url,event'], { cwd: repo });
60
+ if (!r.ok) return { ok: false, runs: [], error: (r.stderr || '').trim().slice(0, 300) };
61
+ return { ok: true, runs: (json(r.stdout) || []).map((x) => markStall(x, Date.now())) };
62
+ }
63
+
64
+ export function markStall(runRow, now) {
65
+ const queuedFor = now - Date.parse(runRow.createdAt || 0);
66
+ return { ...runRow, stalled: runRow.status === 'queued' && queuedFor > STALL_MS };
67
+ }
68
+
69
+ /**
70
+ * Turn a fresh run list into loop beats against what was seen before.
71
+ * `seen` maps run id -> last status. A run first seen already completed
72
+ * is history, not news: it is recorded without a beat.
73
+ */
74
+ export function runBeats(runs, seen) {
75
+ const out = [];
76
+ for (const r of runs) {
77
+ const id = String(r.databaseId);
78
+ const before = seen.get(id);
79
+ const now = r.status;
80
+ seen.set(id, now);
81
+ if (before === now) continue;
82
+ if (before === undefined && now === 'completed') continue;
83
+ const name = r.workflowName || r.displayTitle || '';
84
+ if (now === 'queued' || now === 'waiting' || now === 'requested' || now === 'pending') out.push({ k: 'loop', p: 'dispatch', id, name });
85
+ else if (now === 'in_progress') out.push({ k: 'loop', p: 'run', id, name });
86
+ else if (now === 'completed') out.push({ k: 'loop', p: 'conclude', id, name, conclusion: r.conclusion || 'unknown' });
87
+ }
88
+ return out;
89
+ }
package/server/hub.mjs ADDED
@@ -0,0 +1,104 @@
1
+ // The event hub behind GET /api/events (one SSE stream per tab).
2
+ //
3
+ // - On connect: a `hello` with server time and the full replay (beats and
4
+ // transcript), so a late tab reduces to the same state as an early one.
5
+ // - A `: ping` comment every 20 s keeps proxies and sleeping tabs honest.
6
+ // - Writes are coalesced into 300 ms flushes; consecutive text deltas for
7
+ // the same transcript item merge into one event.
8
+
9
+ export const PING_MS = 20000;
10
+ export const FLUSH_MS = 300;
11
+ const MAX_BEATS = 5000;
12
+ const MAX_TRANSCRIPT = 2000;
13
+
14
+ export function createHub({ pingMs = PING_MS, flushMs = FLUSH_MS } = {}) {
15
+ const clients = new Set();
16
+ const beats = [];
17
+ const transcript = [];
18
+ let queue = [];
19
+ let timer = null;
20
+ let status = { state: 'idle' };
21
+ const extras = {};
22
+
23
+ function frame(type, data) {
24
+ return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
25
+ }
26
+
27
+ function flush() {
28
+ timer = null;
29
+ if (!queue.length) return;
30
+ const out = coalesce(queue).map((e) => frame(e.type, e.data)).join('');
31
+ queue = [];
32
+ for (const res of clients) res.write(out);
33
+ }
34
+
35
+ function emit(type, data) {
36
+ queue.push({ type, data });
37
+ if (!timer) timer = setTimeout(flush, flushMs);
38
+ }
39
+
40
+ function beat(b) {
41
+ beats.push(b);
42
+ if (beats.length > MAX_BEATS) beats.splice(0, beats.length - MAX_BEATS);
43
+ emit('beat', b);
44
+ }
45
+
46
+ function tx(item) {
47
+ if (item.kind === 'delta') {
48
+ const last = transcript[transcript.length - 1];
49
+ if (last && last.kind === 'text' && last.id === item.id) last.text += item.text;
50
+ else transcript.push({ kind: 'text', id: item.id, agent: item.agent, text: item.text });
51
+ } else transcript.push(item);
52
+ if (transcript.length > MAX_TRANSCRIPT) transcript.splice(0, transcript.length - MAX_TRANSCRIPT);
53
+ emit('tx', item);
54
+ }
55
+
56
+ function setStatus(next) {
57
+ status = { ...status, ...next };
58
+ emit('status', status);
59
+ }
60
+
61
+ function set(key, value) {
62
+ extras[key] = value;
63
+ emit(key, value);
64
+ }
65
+
66
+ function attach(req, res) {
67
+ res.writeHead(200, {
68
+ 'content-type': 'text/event-stream; charset=utf-8',
69
+ 'cache-control': 'no-cache, no-transform',
70
+ connection: 'keep-alive',
71
+ 'x-accel-buffering': 'no',
72
+ });
73
+ res.write(frame('hello', { time: Date.now(), beats, transcript, status, ...extras }));
74
+ clients.add(res);
75
+ const ping = setInterval(() => res.write(': ping\n\n'), pingMs);
76
+ req.on('close', () => { clearInterval(ping); clients.delete(res); });
77
+ }
78
+
79
+ function reset() {
80
+ beats.length = 0;
81
+ transcript.length = 0;
82
+ emit('reset', {});
83
+ }
84
+
85
+ function close() {
86
+ if (timer) { clearTimeout(timer); flush(); }
87
+ for (const res of clients) res.end();
88
+ clients.clear();
89
+ }
90
+
91
+ return { emit, beat, tx, setStatus, set, attach, reset, close, flush, get clients() { return clients.size; }, get beats() { return beats; }, get transcript() { return transcript; } };
92
+ }
93
+
94
+ /** Merge consecutive `tx` deltas for the same item; everything else passes in order. */
95
+ export function coalesce(events) {
96
+ const out = [];
97
+ for (const e of events) {
98
+ const last = out[out.length - 1];
99
+ if (e.type === 'tx' && e.data.kind === 'delta' && last && last.type === 'tx' && last.data.kind === 'delta' && last.data.id === e.data.id) {
100
+ last.data = { ...last.data, text: last.data.text + e.data.text };
101
+ } else out.push({ type: e.type, data: e.data });
102
+ }
103
+ return out;
104
+ }
@@ -0,0 +1,89 @@
1
+ // SDK / stream-json messages -> transcript items for the prompt pane.
2
+ // Pure apart from `ctx`, which remembers which assistant messages already
3
+ // streamed as partials (so the full message that follows is not repeated).
4
+
5
+ export function newTxContext() {
6
+ return { msgId: null, streamed: new Set() };
7
+ }
8
+
9
+ export function summarize(name, input = {}) {
10
+ const pick = input.command || input.file_path || input.pattern || input.url || input.description || input.prompt || input.query || '';
11
+ const s = String(pick).replace(/\s+/g, ' ').trim();
12
+ return s.length > 140 ? `${s.slice(0, 137)}...` : s;
13
+ }
14
+
15
+ /** Copy for account errors, keyed by the SDK's assistant error codes. */
16
+ export function friendlyError(code, fallback) {
17
+ switch (code) {
18
+ case 'authentication_failed':
19
+ return 'Claude Code is not logged in. Run `claude` in a terminal, then /login, and try again.';
20
+ case 'billing_error':
21
+ return 'Your Claude account has a billing problem. Check your plan, then try again.';
22
+ case 'rate_limit':
23
+ return 'Rate limited. Wait a minute and send again.';
24
+ case 'overloaded':
25
+ return 'The service is overloaded right now. Try again shortly.';
26
+ default:
27
+ return fallback || `Claude Code reported: ${code}`;
28
+ }
29
+ }
30
+
31
+ export function txFromMessage(msg, ctx) {
32
+ const out = [];
33
+ if (!msg || typeof msg !== 'object') return out;
34
+ const agent = msg.parent_tool_use_id || 'main';
35
+ switch (msg.type) {
36
+ case 'system':
37
+ if (msg.subtype === 'init') out.push({ kind: 'system', text: `session ${String(msg.session_id || '').slice(0, 8)} · ${msg.model || 'model'} · ${(msg.slash_commands || []).length} commands`, session: msg.session_id });
38
+ if (msg.subtype === 'task_notification' && msg.summary) out.push({ kind: 'system', agent: msg.tool_use_id || agent, text: `task ${msg.status}: ${msg.summary}` });
39
+ break;
40
+ case 'stream_event': {
41
+ if (msg.parent_tool_use_id) break;
42
+ const ev = msg.event || {};
43
+ if (ev.type === 'message_start' && ev.message) ctx.msgId = ev.message.id;
44
+ if (ev.type === 'content_block_delta' && ev.delta && ev.delta.type === 'text_delta') {
45
+ if (ctx.msgId) ctx.streamed.add(ctx.msgId);
46
+ out.push({ kind: 'delta', id: `${ctx.msgId || 'm'}:${ev.index ?? 0}`, agent, text: ev.delta.text });
47
+ }
48
+ break;
49
+ }
50
+ case 'assistant': {
51
+ const m = msg.message || {};
52
+ if (msg.error) out.push({ kind: 'error', code: msg.error, text: friendlyError(msg.error) });
53
+ const content = Array.isArray(m.content) ? m.content : [];
54
+ content.forEach((block, i) => {
55
+ if (block.type === 'text' && !msg.parent_tool_use_id && !ctx.streamed.has(m.id) && block.text) {
56
+ out.push({ kind: 'text', id: `${m.id || 'm'}:${i}`, agent, text: block.text });
57
+ }
58
+ if (block.type === 'tool_use') out.push({ kind: 'tool', id: block.id, agent, name: block.name, summary: summarize(block.name, block.input) });
59
+ });
60
+ break;
61
+ }
62
+ case 'user': {
63
+ const content = msg.message && Array.isArray(msg.message.content) ? msg.message.content : [];
64
+ for (const block of content) {
65
+ if (block && block.type === 'tool_result' && block.is_error) out.push({ kind: 'tool-error', id: block.tool_use_id, agent, text: toText(block.content).slice(0, 400) });
66
+ }
67
+ break;
68
+ }
69
+ case 'result':
70
+ out.push({
71
+ kind: 'result',
72
+ ok: !msg.is_error,
73
+ subtype: msg.subtype,
74
+ cost: typeof msg.total_cost_usd === 'number' ? msg.total_cost_usd : null,
75
+ turns: msg.num_turns ?? null,
76
+ ms: msg.duration_ms ?? null,
77
+ session: msg.session_id,
78
+ text: msg.is_error ? toText(msg.result || msg.errors || msg.subtype) : '',
79
+ });
80
+ break;
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function toText(c) {
86
+ if (typeof c === 'string') return c;
87
+ if (Array.isArray(c)) return c.map((x) => (typeof x === 'string' ? x : x && x.text ? x.text : '')).join(' ');
88
+ return c == null ? '' : JSON.stringify(c);
89
+ }
@@ -0,0 +1,59 @@
1
+ // Onboarding preflight: is this machine ready to drive Claude Code here?
2
+ import { run, json } from './sh.mjs';
3
+ import { repoInfo } from './repo.mjs';
4
+
5
+ export async function preflight(repo) {
6
+ const [ver, auth, gh, info] = await Promise.all([
7
+ run('claude', ['--version']),
8
+ run('claude', ['auth', 'status', '--json'], { timeout: 15000 }),
9
+ run('gh', ['auth', 'status']),
10
+ repoInfo(repo),
11
+ ]);
12
+ const a = json(auth.stdout) || {};
13
+ const checks = [
14
+ {
15
+ id: 'claude',
16
+ label: 'Claude Code installed',
17
+ ok: ver.ok,
18
+ detail: ver.ok ? ver.stdout.trim() : 'Not found on PATH.',
19
+ fix: ver.ok ? null : 'Install Claude Code (https://claude.com/claude-code), then reopen this page.',
20
+ },
21
+ {
22
+ id: 'login',
23
+ label: 'Logged in',
24
+ ok: !!a.loggedIn,
25
+ detail: a.loggedIn ? [a.authMethod, a.subscriptionType].filter(Boolean).join(' · ') : 'Not logged in.',
26
+ fix: a.loggedIn ? null : 'Run `claude` in a terminal, then /login.',
27
+ },
28
+ {
29
+ id: 'gh',
30
+ label: 'GitHub CLI signed in',
31
+ ok: gh.ok,
32
+ optional: true,
33
+ detail: gh.ok ? firstLine(gh.stdout + gh.stderr, /Logged in to .*/) : gh.missing ? 'gh not installed.' : 'Not signed in.',
34
+ fix: gh.ok ? null : 'Optional, for the cloud loop and PRs: install gh, then `gh auth login`.',
35
+ },
36
+ {
37
+ id: 'remote',
38
+ label: 'Git remote',
39
+ ok: !!info.remote,
40
+ optional: true,
41
+ detail: info.remote || (info.isGit ? 'No origin remote.' : 'This folder is not a git repo.'),
42
+ fix: info.remote ? null : info.isGit ? 'Add one: `git remote add origin <url>`.' : 'Run the GUI from inside your project, or `git init` first.',
43
+ },
44
+ {
45
+ id: 'adopted',
46
+ label: 'nexus adopted',
47
+ ok: info.adopted,
48
+ optional: true,
49
+ detail: info.adopted ? 'agents.md, plan/ and .claude/commands present.' : 'Not yet.',
50
+ fix: info.adopted ? null : 'Use "Adopt nexus" below, or give the prompt to your agent.',
51
+ },
52
+ ];
53
+ return { checks, info, ready: checks.filter((c) => !c.optional).every((c) => c.ok) };
54
+ }
55
+
56
+ function firstLine(text, re) {
57
+ const m = re.exec(text);
58
+ return m ? m[0].trim() : text.trim().split(/\r?\n/)[0] || '';
59
+ }