nfo-cli 0.0.3 → 0.0.5
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/dist/claude-command.js +6 -1
- package/dist/claude-command.js.map +1 -1
- package/dist/claude-trust.js +46 -0
- package/dist/claude-trust.js.map +1 -0
- package/dist/cli.js +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/mcp/handlers.js +5 -0
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/tool-defs.js +10 -0
- package/dist/mcp/tool-defs.js.map +1 -1
- package/dist/musicians/dismiss.js +1 -1
- package/dist/musicians/dismiss.js.map +1 -1
- package/dist/musicians/roles.js +15 -0
- package/dist/musicians/roles.js.map +1 -0
- package/dist/musicians/spawn.js +53 -18
- package/dist/musicians/spawn.js.map +1 -1
- package/dist/permission.js +14 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/musician-role.js +2 -1
- package/dist/prompts/musician-role.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +42 -8
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +10 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/{App.js → components/App.js} +20 -20
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/{Help.js → components/Help.js} +0 -8
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +8 -1
- package/assets/agent-screen.png +0 -0
- package/assets/main-screen.png +0 -0
- package/assets/orche-clawd.png +0 -0
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js.map +0 -1
- package/dist/tui/OrchestratorPane.js.map +0 -1
- package/dist/tui/SidebarHeader.js +0 -6
- package/dist/tui/SidebarHeader.js.map +0 -1
- package/dist/tui/StatusBar.js.map +0 -1
- package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
- package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
- package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
- package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
- package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
- package/docs/specs/2026-05-29-nfo-design.md +0 -468
- package/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -164
- package/src/commands/attach.ts +0 -24
- package/src/commands/dashboard-window.ts +0 -33
- package/src/commands/kill.ts +0 -50
- package/src/commands/launch.ts +0 -134
- package/src/commands/list.ts +0 -43
- package/src/commands/mcp-server.ts +0 -18
- package/src/commands/notes.ts +0 -18
- package/src/commands/restore.ts +0 -153
- package/src/commands/tui.tsx +0 -16
- package/src/config.ts +0 -44
- package/src/dashboard.ts +0 -1
- package/src/mcp/config.ts +0 -39
- package/src/mcp/handlers.ts +0 -141
- package/src/mcp/server.ts +0 -50
- package/src/mcp/tool-defs.ts +0 -151
- package/src/musicians/dismiss.ts +0 -60
- package/src/musicians/ids.ts +0 -21
- package/src/musicians/lookup.ts +0 -13
- package/src/musicians/message-log.ts +0 -152
- package/src/musicians/message.ts +0 -99
- package/src/musicians/query.ts +0 -19
- package/src/musicians/spawn.ts +0 -139
- package/src/notes.ts +0 -39
- package/src/notify.ts +0 -62
- package/src/orchestrator/report-back.ts +0 -33
- package/src/permission.ts +0 -30
- package/src/project-key.ts +0 -12
- package/src/prompts/musician-role.ts +0 -22
- package/src/prompts/orchestrator-role.ts +0 -60
- package/src/prompts/tool-discipline.ts +0 -35
- package/src/repo.ts +0 -14
- package/src/shell-quote.ts +0 -7
- package/src/state-updaters.ts +0 -132
- package/src/state.ts +0 -49
- package/src/state.types.ts +0 -67
- package/src/tmux.ts +0 -226
- package/src/tui/App.tsx +0 -532
- package/src/tui/AppView.tsx +0 -96
- package/src/tui/Auditorium.tsx +0 -56
- package/src/tui/ConcertHall.tsx +0 -31
- package/src/tui/Help.tsx +0 -72
- package/src/tui/OrchestratorPane.tsx +0 -98
- package/src/tui/SidebarHeader.tsx +0 -32
- package/src/tui/StatusBar.tsx +0 -44
- package/src/tui/activity-line.ts +0 -16
- package/src/tui/detect-permission.ts +0 -93
- package/src/tui/embedded-session-lifecycle.ts +0 -44
- package/src/tui/embedded-terminal.ts +0 -325
- package/src/tui/format-time.ts +0 -25
- package/src/tui/keymap.ts +0 -104
- package/src/tui/poll-activity.ts +0 -25
- package/src/tui/poll-idle.ts +0 -149
- package/src/tui/poll-permission.ts +0 -50
- package/src/tui/status-icon.ts +0 -35
- package/src/tui/terminal-input.ts +0 -136
- package/src/tui/watch-state.ts +0 -43
- package/src/worktree.ts +0 -41
- package/tests/claude-command.test.ts +0 -30
- package/tests/claude-detect.test.ts +0 -14
- package/tests/commands/attach.test.ts +0 -60
- package/tests/commands/kill.test.ts +0 -66
- package/tests/commands/launch.test.ts +0 -75
- package/tests/commands/list.test.ts +0 -47
- package/tests/commands/notes.test.ts +0 -53
- package/tests/commands/restore.test.ts +0 -126
- package/tests/helpers/tmp-config.ts +0 -16
- package/tests/helpers/tmp-repo.ts +0 -29
- package/tests/integration/orchestrator-spawn.test.ts +0 -108
- package/tests/mcp/handlers.test.ts +0 -163
- package/tests/mcp/tool-defs.test.ts +0 -35
- package/tests/musicians/dismiss.test.ts +0 -102
- package/tests/musicians/message.test.ts +0 -159
- package/tests/musicians/query.test.ts +0 -65
- package/tests/musicians/spawn.test.ts +0 -125
- package/tests/notes.test.ts +0 -56
- package/tests/notify.test.ts +0 -80
- package/tests/orchestrator/report-back.test.ts +0 -18
- package/tests/permission.test.ts +0 -29
- package/tests/project-key.test.ts +0 -33
- package/tests/prompts/tool-discipline.test.ts +0 -25
- package/tests/repo.test.ts +0 -38
- package/tests/state-updaters.test.ts +0 -126
- package/tests/state.test.ts +0 -85
- package/tests/tmux.test.ts +0 -126
- package/tests/tui/AppView.test.tsx +0 -92
- package/tests/tui/Auditorium.test.tsx +0 -67
- package/tests/tui/ConcertHall.test.tsx +0 -22
- package/tests/tui/Help.test.tsx +0 -38
- package/tests/tui/OrchestratorPane.test.ts +0 -30
- package/tests/tui/SidebarHeader.test.tsx +0 -20
- package/tests/tui/StatusBar.test.tsx +0 -51
- package/tests/tui/activity-line.test.ts +0 -21
- package/tests/tui/detect-permission.test.ts +0 -92
- package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
- package/tests/tui/embedded-terminal.test.ts +0 -80
- package/tests/tui/format-time.test.ts +0 -25
- package/tests/tui/keymap.test.ts +0 -93
- package/tests/tui/poll-activity.test.ts +0 -81
- package/tests/tui/poll-idle.test.ts +0 -159
- package/tests/tui/poll-permission.test.ts +0 -222
- package/tests/tui/status-icon.test.ts +0 -27
- package/tests/tui/terminal-input.test.ts +0 -113
- package/tests/tui/watch-state.test.ts +0 -54
- package/tests/worktree.test.ts +0 -73
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -12
- /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
- /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
package/src/cli.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { decideAction, createOrchestra } from './commands/launch.js';
|
|
4
|
-
import { attachOrRestore } from './commands/attach.js';
|
|
5
|
-
import { listOrchestras, formatOrchestraList, type OrchestraSummary } from './commands/list.js';
|
|
6
|
-
import { isPermissionLevel, AUTO_CONFIRM_PHRASE, AUTO_WARNING, type PermissionLevel } from './permission.js';
|
|
7
|
-
import { detectClaude } from './claude-detect.js';
|
|
8
|
-
import { createInterface } from 'node:readline/promises';
|
|
9
|
-
|
|
10
|
-
const program = new Command();
|
|
11
|
-
program
|
|
12
|
-
.name('nfo')
|
|
13
|
-
.description('NoFluffOrchestra — TUI multi-agent orchestrator')
|
|
14
|
-
.version('0.0.0');
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.argument('[id]', 'Orchestra id to attach (optional)')
|
|
18
|
-
.option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
|
|
19
|
-
.action(async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
|
|
20
|
-
await detectClaude();
|
|
21
|
-
try {
|
|
22
|
-
if (id) {
|
|
23
|
-
await attachOrRestore(id);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const decision = await decideAction(process.cwd());
|
|
27
|
-
switch (decision.kind) {
|
|
28
|
-
case 'create': {
|
|
29
|
-
const level = await promptPermissionLevel();
|
|
30
|
-
await createOrchestra({
|
|
31
|
-
repoRoot: decision.repoRoot,
|
|
32
|
-
orchestraId: decision.orchestraId,
|
|
33
|
-
permissionLevel: level,
|
|
34
|
-
notifyOnPermission: opts.notifyOnPermission,
|
|
35
|
-
});
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
case 'attach_existing':
|
|
39
|
-
await attachOrRestore(decision.orchestraId);
|
|
40
|
-
return;
|
|
41
|
-
case 'pick': {
|
|
42
|
-
const picked = await promptOrchestraPicker(decision.summaries);
|
|
43
|
-
await attachOrRestore(picked);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
case 'error':
|
|
47
|
-
console.error(decision.message);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
program
|
|
57
|
-
.command('list')
|
|
58
|
-
.description('List all known orchestras')
|
|
59
|
-
.action(async () => {
|
|
60
|
-
const summaries = await listOrchestras();
|
|
61
|
-
console.log(formatOrchestraList(summaries));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
program
|
|
65
|
-
.command('restore <id>')
|
|
66
|
-
.description('Force-restore a stopped orchestra')
|
|
67
|
-
.option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
|
|
68
|
-
.action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
|
|
69
|
-
const { restoreOrchestra } = await import('./commands/restore.js');
|
|
70
|
-
await restoreOrchestra(id, undefined, opts.notifyOnPermission);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
program
|
|
74
|
-
.command('kill <id>')
|
|
75
|
-
.description('Tear down an orchestra (state archived, notes preserved)')
|
|
76
|
-
.option('-y, --yes', 'Skip confirmation prompt')
|
|
77
|
-
.action(async (id: string, opts: { yes?: boolean }) => {
|
|
78
|
-
const { killOrchestra } = await import('./commands/kill.js');
|
|
79
|
-
await killOrchestra(id, opts);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
program
|
|
83
|
-
.command('notes <id>')
|
|
84
|
-
.description('Open the orchestra\'s notes/ directory in $EDITOR')
|
|
85
|
-
.action(async (id: string) => {
|
|
86
|
-
const { openNotes } = await import('./commands/notes.js');
|
|
87
|
-
await openNotes(id);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
program
|
|
91
|
-
.command('mcp-server', { hidden: true })
|
|
92
|
-
.description('(internal) Run the NFO MCP server attached to an orchestra')
|
|
93
|
-
.requiredOption('--orchestra-id <id>', 'Orchestra id')
|
|
94
|
-
.option('--caller-musician-id <id>', 'When the server is hosting a Musician')
|
|
95
|
-
.action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
|
|
96
|
-
const { runMcpServerCli } = await import('./commands/mcp-server.js');
|
|
97
|
-
await runMcpServerCli({
|
|
98
|
-
orchestraId: opts.orchestraId,
|
|
99
|
-
callerMusicianId: opts.callerMusicianId,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
program
|
|
104
|
-
.command('tui', { hidden: true })
|
|
105
|
-
.description('(internal) Run the NFO Ink TUI for an orchestra')
|
|
106
|
-
.requiredOption('--orchestra-id <id>', 'Orchestra id')
|
|
107
|
-
.action(async (opts: { orchestraId: string }) => {
|
|
108
|
-
const { runTui } = await import('./commands/tui.js');
|
|
109
|
-
await runTui({ orchestraId: opts.orchestraId });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
program.parseAsync(process.argv);
|
|
113
|
-
|
|
114
|
-
async function promptPermissionLevel(): Promise<PermissionLevel> {
|
|
115
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
-
try {
|
|
117
|
-
const ans = (await rl.question(
|
|
118
|
-
`Permission level for this orchestra:
|
|
119
|
-
1) auto — RISKY: bypasses all permission checks
|
|
120
|
-
2) autonomous — auto-accept edits, prompt on risky tools
|
|
121
|
-
3) supervised — claude's default prompt-on-risky behavior
|
|
122
|
-
4) strict — read-only / plan mode
|
|
123
|
-
Choose [1-4] (default 3): `,
|
|
124
|
-
)).trim();
|
|
125
|
-
|
|
126
|
-
const map: Record<string, PermissionLevel> = {
|
|
127
|
-
'1': 'auto', '2': 'autonomous', '3': 'supervised', '4': 'strict', '': 'supervised',
|
|
128
|
-
};
|
|
129
|
-
const level = map[ans];
|
|
130
|
-
if (!level || !isPermissionLevel(level)) {
|
|
131
|
-
throw new Error(`Invalid choice: ${ans}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (level === 'auto') {
|
|
135
|
-
console.log('\n' + AUTO_WARNING + '\n');
|
|
136
|
-
const confirm = (await rl.question('> ')).trim();
|
|
137
|
-
if (confirm !== AUTO_CONFIRM_PHRASE) {
|
|
138
|
-
throw new Error('Auto mode not confirmed. Aborting.');
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return level;
|
|
143
|
-
} finally {
|
|
144
|
-
rl.close();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function promptOrchestraPicker(summaries: OrchestraSummary[]): Promise<string> {
|
|
149
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
-
try {
|
|
151
|
-
console.log('Multiple orchestras found:');
|
|
152
|
-
summaries.forEach((s, i) => {
|
|
153
|
-
console.log(` ${i + 1}) ${s.running ? '●' : '○'} ${s.id} (${s.project_path})`);
|
|
154
|
-
});
|
|
155
|
-
const choice = (await rl.question('Pick one [1-N]: ')).trim();
|
|
156
|
-
const idx = Number(choice) - 1;
|
|
157
|
-
if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
|
|
158
|
-
throw new Error('Invalid choice');
|
|
159
|
-
}
|
|
160
|
-
return summaries[idx].id;
|
|
161
|
-
} finally {
|
|
162
|
-
rl.close();
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/commands/attach.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { LaunchResult } from './launch.js';
|
|
2
|
-
import { sessionExists, sessionName, attachSession, ensureNfoSessionUi, selectWindow } from '../tmux.js';
|
|
3
|
-
import { restoreOrchestra } from './restore.js';
|
|
4
|
-
import { readState } from '../state.js';
|
|
5
|
-
import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
|
|
6
|
-
import { ensureDashboardWindow, migrateLegacySidebarPane } from './dashboard-window.js';
|
|
7
|
-
|
|
8
|
-
export async function attachOrRestore(orchestraId: string, dryRun?: boolean): Promise<LaunchResult> {
|
|
9
|
-
const state = await readState(orchestraId);
|
|
10
|
-
if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
|
|
11
|
-
|
|
12
|
-
const name = sessionName(orchestraId);
|
|
13
|
-
if (await sessionExists(name)) {
|
|
14
|
-
await ensureNfoSessionUi(name);
|
|
15
|
-
await ensureDashboardWindow(name, state.project_path, orchestraId);
|
|
16
|
-
await migrateLegacySidebarPane(name);
|
|
17
|
-
if (!dryRun) {
|
|
18
|
-
await selectWindow(name, DASHBOARD_WINDOW_NAME);
|
|
19
|
-
await attachSession(name);
|
|
20
|
-
}
|
|
21
|
-
return { action: 'attached', orchestraId };
|
|
22
|
-
}
|
|
23
|
-
return restoreOrchestra(orchestraId, dryRun);
|
|
24
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
|
|
3
|
-
import { createDetachedWindow, respawnPane, setPaneOption } from '../tmux.js';
|
|
4
|
-
import { shellQuote } from '../shell-quote.js';
|
|
5
|
-
|
|
6
|
-
function tuiCommand(orchestraId: string): string {
|
|
7
|
-
const nfoBin = process.argv[1];
|
|
8
|
-
return `${shellQuote(nfoBin)} tui --orchestra-id ${shellQuote(orchestraId)}`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function ensureDashboardWindow(
|
|
12
|
-
session: string,
|
|
13
|
-
cwd: string,
|
|
14
|
-
orchestraId: string,
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
await removeDashboardWindow(session);
|
|
17
|
-
const paneId = await createDetachedWindow(session, DASHBOARD_WINDOW_NAME, cwd);
|
|
18
|
-
await setPaneOption(paneId, 'remain-on-exit', 'on');
|
|
19
|
-
await respawnPane(paneId, tuiCommand(orchestraId));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function migrateLegacySidebarPane(session: string): Promise<void> {
|
|
23
|
-
await execa('tmux', ['kill-pane', '-t', `${session}:0.1`], { reject: false });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function removeDashboardWindow(session: string): Promise<void> {
|
|
27
|
-
const { stdout } = await execa('tmux', ['list-windows', '-t', session, '-F', '#{window_name}']);
|
|
28
|
-
const names = stdout.split('\n').map((line) => { return line.trim(); }).filter(Boolean);
|
|
29
|
-
if (!names.includes(DASHBOARD_WINDOW_NAME)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
await execa('tmux', ['kill-window', '-t', `${session}:${DASHBOARD_WINDOW_NAME}`], { reject: false });
|
|
33
|
-
}
|
package/src/commands/kill.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline/promises';
|
|
2
|
-
import { rename, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { readState } from '../state.js';
|
|
6
|
-
import {
|
|
7
|
-
sessionName,
|
|
8
|
-
sessionExists,
|
|
9
|
-
killSession,
|
|
10
|
-
} from '../tmux.js';
|
|
11
|
-
import { archiveDir, stateFile } from '../config.js';
|
|
12
|
-
|
|
13
|
-
export interface KillOptions {
|
|
14
|
-
yes?: boolean; // skip confirmation prompt
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function killOrchestra(orchestraId: string, opts: KillOptions = {}): Promise<void> {
|
|
18
|
-
const state = await readState(orchestraId);
|
|
19
|
-
if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
|
|
20
|
-
|
|
21
|
-
if (!opts.yes) {
|
|
22
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
-
try {
|
|
24
|
-
const ans = (await rl.question(
|
|
25
|
-
`Kill orchestra ${orchestraId} (${state.project_path})? [y/N] `,
|
|
26
|
-
)).trim().toLowerCase();
|
|
27
|
-
if (ans !== 'y' && ans !== 'yes') {
|
|
28
|
-
console.log('Aborted.');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
} finally {
|
|
32
|
-
rl.close();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Phase 1: no musicians (and therefore no worktrees to handle).
|
|
37
|
-
// Phase 2 will add the worktree-archive prompt.
|
|
38
|
-
|
|
39
|
-
const name = sessionName(orchestraId);
|
|
40
|
-
if (await sessionExists(name)) {
|
|
41
|
-
await killSession(name);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Archive state.json under archive/state-<timestamp>.json so notes/ stays intact.
|
|
45
|
-
await mkdir(archiveDir(orchestraId), { recursive: true });
|
|
46
|
-
const archived = join(archiveDir(orchestraId), `state-${Date.now()}.json`);
|
|
47
|
-
if (existsSync(stateFile(orchestraId))) {
|
|
48
|
-
await rename(stateFile(orchestraId), archived);
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/commands/launch.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { resolveRepoRoot } from '../repo.js';
|
|
4
|
-
import { projectKeyFromPath } from '../project-key.js';
|
|
5
|
-
import { ensureOrchestraDir, readState, writeState } from '../state.js';
|
|
6
|
-
import { makeInitialState } from '../state.types.js';
|
|
7
|
-
import {
|
|
8
|
-
claudeFlagsForLevel,
|
|
9
|
-
type PermissionLevel,
|
|
10
|
-
} from '../permission.js';
|
|
11
|
-
import {
|
|
12
|
-
sessionName,
|
|
13
|
-
sessionExists,
|
|
14
|
-
createDetachedSession,
|
|
15
|
-
attachSession,
|
|
16
|
-
selectWindow,
|
|
17
|
-
ensureNfoSessionUi,
|
|
18
|
-
respawnPane,
|
|
19
|
-
setPaneOption,
|
|
20
|
-
} from '../tmux.js';
|
|
21
|
-
import { ORCHESTRATOR_ROLE_PROMPT_V1 } from '../prompts/orchestrator-role.js';
|
|
22
|
-
import { orchestraDir } from '../config.js';
|
|
23
|
-
import { listOrchestras } from './list.js';
|
|
24
|
-
import type { OrchestraSummary } from './list.js';
|
|
25
|
-
import { noteRead, noteList } from '../notes.js';
|
|
26
|
-
import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
|
|
27
|
-
import { ensureDashboardWindow } from './dashboard-window.js';
|
|
28
|
-
import { buildClaudeCommand } from '../claude-command.js';
|
|
29
|
-
import { writeOrchestratorMcpConfig } from '../mcp/config.js';
|
|
30
|
-
|
|
31
|
-
export interface LaunchOptions {
|
|
32
|
-
cwd: string;
|
|
33
|
-
interactive?: boolean; // when false, must supply permissionLevel
|
|
34
|
-
permissionLevel?: PermissionLevel;
|
|
35
|
-
dryRun?: boolean; // when true, do not attach
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface LaunchResult {
|
|
39
|
-
action: 'created' | 'attached' | 'restored';
|
|
40
|
-
orchestraId: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type LaunchDecision =
|
|
44
|
-
| { kind: 'create'; orchestraId: string; repoRoot: string }
|
|
45
|
-
| { kind: 'attach_existing'; orchestraId: string }
|
|
46
|
-
| { kind: 'pick'; summaries: OrchestraSummary[] }
|
|
47
|
-
| { kind: 'error'; message: string };
|
|
48
|
-
|
|
49
|
-
export async function decideAction(cwd: string): Promise<LaunchDecision> {
|
|
50
|
-
const repoRoot = await resolveRepoRoot(cwd);
|
|
51
|
-
|
|
52
|
-
if (repoRoot) {
|
|
53
|
-
const orchestraId = projectKeyFromPath(repoRoot);
|
|
54
|
-
const existing = await readState(orchestraId);
|
|
55
|
-
if (existing) {
|
|
56
|
-
return { kind: 'attach_existing', orchestraId };
|
|
57
|
-
}
|
|
58
|
-
return { kind: 'create', orchestraId, repoRoot };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Out of repo. Inspect known orchestras.
|
|
62
|
-
const summaries = await listOrchestras();
|
|
63
|
-
if (summaries.length === 0) {
|
|
64
|
-
return { kind: 'error', message: 'Open NFO in a git repository to create your first orchestra.' };
|
|
65
|
-
}
|
|
66
|
-
const running = summaries.filter(s => s.running);
|
|
67
|
-
if (running.length === 1) {
|
|
68
|
-
return { kind: 'attach_existing', orchestraId: running[0].id };
|
|
69
|
-
}
|
|
70
|
-
return { kind: 'pick', summaries };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface CreateOrchestraOptions {
|
|
74
|
-
repoRoot: string;
|
|
75
|
-
orchestraId: string;
|
|
76
|
-
permissionLevel: PermissionLevel;
|
|
77
|
-
dryRun?: boolean;
|
|
78
|
-
notifyOnPermission?: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function createOrchestra(opts: CreateOrchestraOptions): Promise<LaunchResult> {
|
|
82
|
-
await ensureOrchestraDir(opts.orchestraId);
|
|
83
|
-
const state = makeInitialState({
|
|
84
|
-
orchestraId: opts.orchestraId,
|
|
85
|
-
projectPath: opts.repoRoot,
|
|
86
|
-
permissionLevel: opts.permissionLevel,
|
|
87
|
-
notifyOnPermission: opts.notifyOnPermission,
|
|
88
|
-
});
|
|
89
|
-
await writeState(opts.orchestraId, state);
|
|
90
|
-
|
|
91
|
-
const mcpConfigPath = await writeOrchestratorMcpConfig(opts.orchestraId);
|
|
92
|
-
|
|
93
|
-
const promptFile = join(orchestraDir(opts.orchestraId), 'orchestrator-prompt.md');
|
|
94
|
-
const notes = await loadOrchestratorNotes(opts.orchestraId);
|
|
95
|
-
await writeFile(promptFile, ORCHESTRATOR_ROLE_PROMPT_V1 + notes, 'utf8');
|
|
96
|
-
|
|
97
|
-
const name = sessionName(opts.orchestraId);
|
|
98
|
-
await createDetachedSession(name, opts.repoRoot);
|
|
99
|
-
await ensureNfoSessionUi(name);
|
|
100
|
-
await setPaneOption(`${name}:0`, 'remain-on-exit', 'on');
|
|
101
|
-
|
|
102
|
-
await ensureDashboardWindow(name, opts.repoRoot, opts.orchestraId);
|
|
103
|
-
|
|
104
|
-
const claudeFlags = claudeFlagsForLevel(opts.permissionLevel);
|
|
105
|
-
const claudeCmd = buildClaudeCommand({
|
|
106
|
-
flags: claudeFlags,
|
|
107
|
-
mcpConfigPath,
|
|
108
|
-
promptFile,
|
|
109
|
-
});
|
|
110
|
-
await respawnPane(`${name}:0`, claudeCmd);
|
|
111
|
-
|
|
112
|
-
if (!opts.dryRun) {
|
|
113
|
-
await selectWindow(name, DASHBOARD_WINDOW_NAME);
|
|
114
|
-
await attachSession(name);
|
|
115
|
-
}
|
|
116
|
-
return { action: 'created', orchestraId: opts.orchestraId };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function loadOrchestratorNotes(orchestraId: string): Promise<string> {
|
|
120
|
-
const files = await noteList(orchestraId);
|
|
121
|
-
const ordered = ['overview.md', 'decisions.md'].filter((f) => { return files.includes(f); });
|
|
122
|
-
if (ordered.length === 0) {
|
|
123
|
-
return '';
|
|
124
|
-
}
|
|
125
|
-
const parts: string[] = ['\n\n## Curated project notes (loaded from notes/)\n'];
|
|
126
|
-
for (const f of ordered) {
|
|
127
|
-
const content = await noteRead(orchestraId, f);
|
|
128
|
-
if (content.trim().length === 0) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
parts.push(`\n### ${f}\n\n${content}\n`);
|
|
132
|
-
}
|
|
133
|
-
return parts.join('');
|
|
134
|
-
}
|
package/src/commands/list.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { getProjectsDir } from '../config.js';
|
|
4
|
-
import { readState } from '../state.js';
|
|
5
|
-
import { sessionExists, sessionName } from '../tmux.js';
|
|
6
|
-
|
|
7
|
-
export interface OrchestraSummary {
|
|
8
|
-
id: string;
|
|
9
|
-
project_path: string;
|
|
10
|
-
permission_level: string;
|
|
11
|
-
created_at: string;
|
|
12
|
-
running: boolean;
|
|
13
|
-
musician_count: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function listOrchestras(): Promise<OrchestraSummary[]> {
|
|
17
|
-
const projectsDir = getProjectsDir();
|
|
18
|
-
if (!existsSync(projectsDir)) return [];
|
|
19
|
-
const dirs = await readdir(projectsDir, { withFileTypes: true });
|
|
20
|
-
const summaries: OrchestraSummary[] = [];
|
|
21
|
-
for (const d of dirs) {
|
|
22
|
-
if (!d.isDirectory()) continue;
|
|
23
|
-
const state = await readState(d.name);
|
|
24
|
-
if (!state) continue;
|
|
25
|
-
summaries.push({
|
|
26
|
-
id: state.orchestra_id,
|
|
27
|
-
project_path: state.project_path,
|
|
28
|
-
permission_level: state.permission_level,
|
|
29
|
-
created_at: state.created_at,
|
|
30
|
-
running: await sessionExists(sessionName(state.orchestra_id)),
|
|
31
|
-
musician_count: state.musicians.length,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return summaries;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function formatOrchestraList(summaries: OrchestraSummary[]): string {
|
|
38
|
-
if (summaries.length === 0) return 'No orchestras found.';
|
|
39
|
-
const rows = summaries.map(s =>
|
|
40
|
-
`${s.running ? '●' : '○'} ${s.id}\n ${s.project_path}\n level=${s.permission_level} musicians=${s.musician_count}`,
|
|
41
|
-
);
|
|
42
|
-
return rows.join('\n\n');
|
|
43
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { runServer } from '../mcp/server.js';
|
|
2
|
-
import { readState } from '../state.js';
|
|
3
|
-
|
|
4
|
-
export interface McpServerCliOptions {
|
|
5
|
-
orchestraId: string;
|
|
6
|
-
callerMusicianId?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function runMcpServerCli(opts: McpServerCliOptions): Promise<void> {
|
|
10
|
-
const state = await readState(opts.orchestraId);
|
|
11
|
-
if (!state) {
|
|
12
|
-
throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
|
|
13
|
-
}
|
|
14
|
-
await runServer({
|
|
15
|
-
orchestraId: opts.orchestraId,
|
|
16
|
-
callerMusicianId: opts.callerMusicianId,
|
|
17
|
-
});
|
|
18
|
-
}
|
package/src/commands/notes.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { execa } from 'execa';
|
|
3
|
-
import { notesDir } from '../config.js';
|
|
4
|
-
import { readState } from '../state.js';
|
|
5
|
-
|
|
6
|
-
export async function openNotes(orchestraId: string): Promise<void> {
|
|
7
|
-
const state = await readState(orchestraId);
|
|
8
|
-
if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
|
|
9
|
-
|
|
10
|
-
const dir = notesDir(orchestraId);
|
|
11
|
-
if (!existsSync(dir)) {
|
|
12
|
-
throw new Error(`Notes directory missing for ${orchestraId}: ${dir}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const editor = process.env.EDITOR ?? 'vi';
|
|
16
|
-
// Open the dir, not a specific file — the user picks which note to edit.
|
|
17
|
-
await execa(editor, [dir], { stdio: 'inherit' });
|
|
18
|
-
}
|
package/src/commands/restore.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { execa } from "execa";
|
|
5
|
-
import { readState, writeState } from "../state.js";
|
|
6
|
-
import { setMusicianTmuxWindowId } from "../state-updaters.js";
|
|
7
|
-
import { orchestraDir } from "../config.js";
|
|
8
|
-
import {
|
|
9
|
-
sessionName,
|
|
10
|
-
sessionExists,
|
|
11
|
-
createDetachedSession,
|
|
12
|
-
attachSession,
|
|
13
|
-
ensureNfoSessionUi,
|
|
14
|
-
selectWindow,
|
|
15
|
-
respawnPane,
|
|
16
|
-
setPaneOption,
|
|
17
|
-
} from "../tmux.js";
|
|
18
|
-
import { claudeFlagsForLevel } from "../permission.js";
|
|
19
|
-
import { ORCHESTRATOR_ROLE_PROMPT_V1 } from "../prompts/orchestrator-role.js";
|
|
20
|
-
import { MUSICIAN_ROLE_PROMPT_V1 } from "../prompts/musician-role.js";
|
|
21
|
-
import { loadOrchestratorNotes } from "./launch.js";
|
|
22
|
-
import type { LaunchResult } from "./launch.js";
|
|
23
|
-
import { DASHBOARD_WINDOW_NAME } from "../dashboard.js";
|
|
24
|
-
import {
|
|
25
|
-
ensureDashboardWindow,
|
|
26
|
-
migrateLegacySidebarPane,
|
|
27
|
-
} from "./dashboard-window.js";
|
|
28
|
-
import { buildClaudeCommand } from "../claude-command.js";
|
|
29
|
-
import {
|
|
30
|
-
orchestratorMcpConfigPath,
|
|
31
|
-
writeMusicianMcpConfig,
|
|
32
|
-
writeOrchestratorMcpConfig,
|
|
33
|
-
} from "../mcp/config.js";
|
|
34
|
-
|
|
35
|
-
function sanitiseName(name: string): string {
|
|
36
|
-
const cleaned = name
|
|
37
|
-
.toLowerCase()
|
|
38
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
39
|
-
.replace(/-+/g, "-")
|
|
40
|
-
.replace(/^-|-$/g, "")
|
|
41
|
-
.slice(0, 32);
|
|
42
|
-
if (cleaned.length === 0) {
|
|
43
|
-
return "musician";
|
|
44
|
-
}
|
|
45
|
-
return cleaned;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function restoreOrchestra(
|
|
49
|
-
orchestraId: string,
|
|
50
|
-
dryRun?: boolean,
|
|
51
|
-
notifyOnPermission?: boolean,
|
|
52
|
-
): Promise<LaunchResult> {
|
|
53
|
-
const state = await readState(orchestraId);
|
|
54
|
-
if (!state) {
|
|
55
|
-
throw new Error(`Unknown orchestra: ${orchestraId}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (notifyOnPermission !== undefined) {
|
|
59
|
-
state.notify_on_permission = notifyOnPermission;
|
|
60
|
-
await writeState(orchestraId, state);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const name = sessionName(orchestraId);
|
|
64
|
-
if (await sessionExists(name)) {
|
|
65
|
-
await ensureNfoSessionUi(name);
|
|
66
|
-
await ensureDashboardWindow(name, state.project_path, orchestraId);
|
|
67
|
-
await migrateLegacySidebarPane(name);
|
|
68
|
-
if (!dryRun) {
|
|
69
|
-
await selectWindow(name, DASHBOARD_WINDOW_NAME);
|
|
70
|
-
await attachSession(name);
|
|
71
|
-
}
|
|
72
|
-
return { action: "attached", orchestraId };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await createDetachedSession(name, state.project_path);
|
|
76
|
-
await ensureNfoSessionUi(name);
|
|
77
|
-
await ensureDashboardWindow(name, state.project_path, orchestraId);
|
|
78
|
-
await setPaneOption(`${name}:0`, "remain-on-exit", "on");
|
|
79
|
-
|
|
80
|
-
const mcpConfigPath = existsSync(orchestratorMcpConfigPath(orchestraId))
|
|
81
|
-
? orchestratorMcpConfigPath(orchestraId)
|
|
82
|
-
: await writeOrchestratorMcpConfig(orchestraId);
|
|
83
|
-
const flags = claudeFlagsForLevel(state.permission_level);
|
|
84
|
-
|
|
85
|
-
// Rebuild the Orchestrator's prompt file with current notes content.
|
|
86
|
-
const promptFile = join(orchestraDir(orchestraId), "orchestrator-prompt.md");
|
|
87
|
-
if (existsSync(promptFile)) {
|
|
88
|
-
const notes = await loadOrchestratorNotes(orchestraId);
|
|
89
|
-
await writeFile(promptFile, ORCHESTRATOR_ROLE_PROMPT_V1 + notes, "utf8");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
await respawnPane(
|
|
93
|
-
`${name}:0`,
|
|
94
|
-
buildClaudeCommand({
|
|
95
|
-
flags,
|
|
96
|
-
resumeSessionId: state.orchestrator_session_id,
|
|
97
|
-
mcpConfigPath,
|
|
98
|
-
promptFile: existsSync(promptFile) ? promptFile : undefined,
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// Restore musicians (Phase 2). Stopped musicians are not restored.
|
|
103
|
-
for (const musician of state.musicians) {
|
|
104
|
-
if (musician.status === "stopped") {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const workingDir = musician.worktree_path ?? state.project_path;
|
|
108
|
-
const winLabel = `mus-${musician.id}-${sanitiseName(musician.name)}`;
|
|
109
|
-
const created = await execa("tmux", [
|
|
110
|
-
"new-window",
|
|
111
|
-
"-t",
|
|
112
|
-
name,
|
|
113
|
-
"-n",
|
|
114
|
-
winLabel,
|
|
115
|
-
"-c",
|
|
116
|
-
workingDir,
|
|
117
|
-
"-d",
|
|
118
|
-
"-P",
|
|
119
|
-
"-F",
|
|
120
|
-
"#{window_id}",
|
|
121
|
-
]);
|
|
122
|
-
const newWindowId = created.stdout.trim();
|
|
123
|
-
// The recreated window has a new id; persist it so message/query target it.
|
|
124
|
-
await setMusicianTmuxWindowId(orchestraId, musician.id, newWindowId);
|
|
125
|
-
|
|
126
|
-
const musicianPromptFile = join(
|
|
127
|
-
orchestraDir(orchestraId),
|
|
128
|
-
`musician-${musician.id}-prompt.md`,
|
|
129
|
-
);
|
|
130
|
-
await writeFile(musicianPromptFile, MUSICIAN_ROLE_PROMPT_V1, "utf8");
|
|
131
|
-
const musicianMcpConfigPath = await writeMusicianMcpConfig(
|
|
132
|
-
orchestraId,
|
|
133
|
-
musician.id,
|
|
134
|
-
);
|
|
135
|
-
await setPaneOption(`${name}:${newWindowId}`, "remain-on-exit", "on");
|
|
136
|
-
await respawnPane(
|
|
137
|
-
`${name}:${newWindowId}`,
|
|
138
|
-
buildClaudeCommand({
|
|
139
|
-
flags,
|
|
140
|
-
resumeSessionId: musician.claude_session_id,
|
|
141
|
-
mcpConfigPath: musicianMcpConfigPath,
|
|
142
|
-
promptFile: musicianPromptFile,
|
|
143
|
-
model: musician.model ?? "sonnet",
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!dryRun) {
|
|
149
|
-
await selectWindow(name, DASHBOARD_WINDOW_NAME);
|
|
150
|
-
await attachSession(name);
|
|
151
|
-
}
|
|
152
|
-
return { action: "restored", orchestraId };
|
|
153
|
-
}
|
package/src/commands/tui.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { render } from 'ink';
|
|
2
|
-
import { App } from '../tui/App.js';
|
|
3
|
-
import { readState } from '../state.js';
|
|
4
|
-
|
|
5
|
-
export interface RunTuiOptions {
|
|
6
|
-
orchestraId: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function runTui(opts: RunTuiOptions): Promise<void> {
|
|
10
|
-
const state = await readState(opts.orchestraId);
|
|
11
|
-
if (!state) {
|
|
12
|
-
throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
|
|
13
|
-
}
|
|
14
|
-
const instance = render(<App orchestraId={opts.orchestraId} />, { exitOnCtrlC: false });
|
|
15
|
-
await instance.waitUntilExit();
|
|
16
|
-
}
|